I have a component like this:
const MyInput = ({ defaultValue, id, onChange }) => {
const [value, setValue] = useState(defaultValue);
const handleChange = (e) => {
const value = e.target.value;
setValue(value);
onChange(id, value);
};
return (
<div>
<input type="text" value={value} onChange={handleChange} />
</div>
);
};
Where I want to it to behave like an uncontrolled component, but I want it to have an initial value that we can set.
In my parent component this works fine,
export default function App() {
const [formState, setFormState] = useState({});
const handleChange = (id, value) => {
console.log(id, value, JSON.stringify(formState)); // For debugging later
setFormState({ ...formState, [id]: value });
};
return (
<div className="App">
<pre>{JSON.stringify(formState, null, 2)}</pre>
<MyInput id="1" defaultValue="one" onChange={handleChange} />
<MyInput id="2" defaultValue="two" onChange={handleChange} />
<MyInput id="3" defaultValue="three" onChange={handleChange} />
<MyInput id="4" defaultValue="four" onChange={handleChange} />
<MyInput id="5" defaultValue="five" onChange={handleChange} />
</div>
);
}
but the problem is that the value of formState
doesn't reflect the values on the MyInputs
until they fire an onchange event.
So ok, I can just add a 'component did mount' useEffect hook on the MyInput to fire an onChange when the component first mounts.
const MyInput = ({ defaultValue, id, onChange }) => {
useEffect(() => {
onChange(id, defaultValue);
}, []);
const [value, setValue] = useState(defaultValue);
const handleChange = (e) => {
const value = e.target.value;
setValue(value);
onChange(id, value);
};
return (
<div>
<input type="text" value={value} onChange={handleChange} />
</div>
);
};
But this doesn't behave how we want. Each of the MyInputs do call onChange when they first mount, but reference to formState
is stale for the main onChange handler in each case.
ie. the console.log output when the page first loads is:
1 one {}
2 two {}
3 three {}
4 four {}
5 five {}
So I've thought that using a ref could solve this. eg:
export default function App() {
const [formState, setFormState] = useState({});
const formStateRef = useRef(formState);
useEffect(() => {
formStateRef.current = formState;
}, [formState]);
const handleChange = (id, value) => {
console.log(
id,
value,
JSON.stringify(formState),
JSON.stringify(formStateRef)
);
setFormState({ ...formStateRef.current, [id]: value });
};
return (
<div className="App">
<pre>{JSON.stringify(formState, null, 2)}</pre>
<MyInput id="1" defaultValue="one" onChange={handleChange} />
<MyInput id="2" defaultValue="two" onChange={handleChange} />
<MyInput id="3" defaultValue="three" onChange={handleChange} />
<MyInput id="4" defaultValue="four" onChange={handleChange} />
<MyInput id="5" defaultValue="five" onChange={handleChange} />
</div>
);
}
This doesn't work. It appears that all of the change handlers fire before useEffect on formState has the chance to update the ref.
1 one {} {"current":{}}
2 two {} {"current":{}}
3 three {} {"current":{}}
4 four {} {"current":{}}
5 five {} {"current":{}}
I guess I could directly mutate the ref in the handleChange function, and set that?
export default function App() {
const [formState, setFormState] = useState({});
const formStateRef = useRef(formState);
const handleChange = (id, value) => {
console.log(
id,
value,
JSON.stringify(formState),
JSON.stringify(formStateRef)
);
formStateRef.current = { ...formStateRef.current, [id]: value };
setFormState(formStateRef.current);
};
return (
<div className="App">
<pre>{JSON.stringify(formState, null, 2)}</pre>
<MyInput id="1" defaultValue="one" onChange={handleChange} />
<MyInput id="2" defaultValue="two" onChange={handleChange} />
<MyInput id="3" defaultValue="three" onChange={handleChange} />
<MyInput id="4" defaultValue="four" onChange={handleChange} />
<MyInput id="5" defaultValue="five" onChange={handleChange} />
</div>
);
}
This does work. But this use of refs makes me a bit uneasy.
What's the standard way to achieve something like this?
If you take your first example and simply use a functional state update instead I think you'll achieve what you are after.
With the the non-functional update, all the inputs mount at the same time and all invoke their onChange
handlers. The handleChange
callback uses the state from the render cycle it was enclosed in. When all inputs enqueue updates in the same render cycle, each subsequent update overwrites the previous state update. The last input to update state is the one that "wins". This is why you see:
{
"5": "five"
}
Instead of
{
"1": "one",
"2": "two",
"3": "three",
"4": "four",
"5": "five"
}
Use a functional state update to correctly update from the previous state, not the state from the previous render cycle.
setFormState(formState => ({ ...formState, [id]: value }));
From here suggestions are only optimizations
Curry the input id in the handler, makes for one less props to pass and process. The children inputs also don't need to know it to handle changes.
const handleChange = (id) => (value) => { setFormState((formState) => ({...formState, [id]: value })); };
Pass defaultValue
to the defaultValue
prop of the underlying inputs.
const MyInput = ({ defaultValue, onChange }) => { useEffect(() => { onChange(defaultValue); }, []); const handleChange = (e) => { const { value } = e.target; onChange(value); }; return ( <div> <input type="text" defaultValue={defaultValue} onChange={handleChange} /> </div> ); };
Pass the id
in the curried handler
<MyInput defaultValue="one" onChange={handleChange(1)} /> <MyInput defaultValue="two" onChange={handleChange(2)} /> <MyInput defaultValue="three" onChange={handleChange(3)} /> <MyInput defaultValue="four" onChange={handleChange(4)} /> <MyInput defaultValue="five" onChange={handleChange(5)} />
demo code
const MyInput = ({ defaultValue, onChange }) => {
useEffect(() => {
onChange(defaultValue);
}, []);
const handleChange = (e) => {
const { value } = e.target;
onChange(value);
};
return (
<div>
<input type="text" defaultValue={defaultValue} onChange={handleChange} />
</div>
);
};
export default function App() {
const [formState, setFormState] = useState({});
const handleChange = (id) => (value) => {
setFormState((formState) => ({ ...formState, [id]: value }));
};
return (
<div className="App">
<pre>{JSON.stringify(formState, null, 2)}</pre>
<MyInput defaultValue="one" onChange={handleChange(1)} />
<MyInput defaultValue="two" onChange={handleChange(2)} />
<MyInput defaultValue="three" onChange={handleChange(3)} />
<MyInput defaultValue="four" onChange={handleChange(4)} />
<MyInput defaultValue="five" onChange={handleChange(5)} />
</div>
);
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.