adobe / adobe/react-spectrum

useNumberField to update value instantly like useTextField (potential solution)

Open
#7,984 8 comments 2 reactions 0 assignees View on GitHub
NumberField
Dominant language
TypeScript
Stars
15.9k
Forks
1.6k
Avg merge
3d 9m
Merged PRs (30d)
59

Description

### Provide a general summary of the feature here

I like a lot of things with this set of packages but the lack of a clean way to handle instant `onChange` events on the numeric inputs is a big downside. There are many use cases like autosave forms that don't require the user to leave the input to make changes to the page that make it so `onBlur` never really happens.

For example, my company has a lot of customizable reports, the user may change the value of a field at the top of the report and expects the report to dynamically change with their new value. There isn't a button to submit the changes, and usually users will just look at the form updates, there is not clicking or anything else that would cause an onBlur event from happening.

### 🤔 Expected Behavior?

I would like to see some sort of option of sending onChange events when the value changes. I've read the docs and I understand why FORMATTING needs to happen onBlur, but it doesn't explain why onChange needs to only happen onBlur. I do understand the complexities of it and as the UI/UX guy on my team I definitely understand the UX side of it. The UX stuff is actually why I am using this package (the input masking is great). I'm hoping this issue can help bring parity of react state management and what the user sees (I see the value in the input, it should also be in react state then).

### 😯 Current Behavior

onChange is only called onBlur (when the user clicks or tabs out of the input).

### 💁 Possible Solution

So I wrote this custom component using your hooks that accomplishes this and am sharing it because there are several other people who are struggling with this. I also hope that this might be something the devs can use to add this functionality without introducing too much complexity.

The idea is pretty basic, essentially you have an internal state that is used for the `value` and `onChange` props of a controlled numeric input. If the input is currently focused and the `state.numberValue` changes, push those changes to the actual `onChange` prop that the developer provided. If the input is not focused, then keep the internal value and the developer provided value in sync. You only change the internal value when the useNumberField onChange event happens or when the user is not focused on the input.

In this way, you can be pushing the instant changes to the developer while maintaining an internal value to prevent formatting from happening until the onBlur event.

```js
const NumberInput = ({ onChange: onExternalChange, value: externalValue, ...props }) => {
const ref = React.useRef(null);
const { locale } = useLocale();
const [internalValue, setInternalValue] = React.useState(externalValue);
const [isFocused, setFocused] = React.useState(false);

const onInternalChange = (newValue) => {
if (onExternalChange) onExternalChange(newValue);
setInternalValue(newValue);
}

/*
Need to memoize this or this will cause the "state.numberValue" to revert to a NaN and not allow you to type in anything.
I actually think this is a bug in either the useNumberFieldState or useNumberField hooks. I noticed if a rerender happens the value can
get replaced with a NaN value and not let you set the value at all. All input gets reset back to NaN. This doesn't seem like correct behavior.
*/
const inputFieldProps = React.useMemo(() => ({
...props,
onChange: onInternalChange,
value: internalValue
}), [internalValue]);

const state = useNumberFieldState({ ...inputFieldProps, locale });
const { inputProps } = useNumberField(inputFieldProps, state, ref);

const { focusWithinProps } = useFocusWithin({
...props,
onFocusWithin(e) { setFocused(true); },
onBlurWithin(e) { setFocused(false); },
});

// change the external value only while typing in the input whenever the numeric value changes.
useEffect(() => {
if (isFocused && onExternalChange) onExternalChange(state.numberValue);
}, [state.numberValue]);

// change the internal value only when not focused on the input.
useEffect(() => {
if (!isFocused) setInternalValue(externalValue);
}, [externalValue]);

return (




);
};
```

Now you can use it like this:

```js
const MyComponent = () => {
const [value, setValue] = useState(0);

return (


The value is: {value}




)
}
```

NOTE: I noticed a bit of a weird bug with the numberField hooks that require them to be memoized or all input gets converted into NaN. This might be related to #1893

### 🔦 Context

n/a

### 💻 Examples

_No response_

### 🧢 Your Company/Team

_No response_

### 🕷 Tracking Issue

_No response_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.