[英]Closure on the result of a `useState` hook causes annoying behaviour
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.但问题是 formState 的值在
MyInputs
onchange 事件之前不会反映formState
上的值。
So ok, I can just add a 'component did mount' useEffect hook on the MyInput to fire an onChange when the component first mounts.好吧,我可以在 MyInput 上添加一个“组件确实安装”useEffect 挂钩,以便在组件首次安装时触发 onChange。
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.每个 MyInputs 在首次挂载时都会调用 onChange,但在每种情况下,对主 onChange 处理程序的
formState
的引用都是陈旧的。
ie. IE。 the console.log output when the page first loads is:
页面首次加载时的 console.log output 是:
1 one {}
2 two {}
3 three {}
4 four {}
5 five {}
So I've thought that using a ref could solve this.所以我认为使用 ref 可以解决这个问题。 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.
似乎所有更改处理程序都会在 formState 上的 useEffect 有机会更新 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?我想我可以直接改变handleChange function中的ref,然后设置它?
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.
但是这种对 refs 的使用让我有点不安。
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.如果您举第一个示例并简单地使用功能性 state 更新,我认为您将实现您所追求的。
With the the non-functional update, all the inputs mount at the same time and all invoke their onChange
handlers.通过非功能性更新,所有输入同时挂载并调用它们的
onChange
处理程序。 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. handleChange
回调使用包含在其中的渲染周期中的 state。当所有输入在同一渲染周期中排队更新时,每个后续更新都会覆盖先前的 state 更新。 The last input to update state is the one that "wins".更新 state 的最后一个输入是“获胜”的输入。 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.使用功能性 state 更新以正确更新前一个 state,而不是前一个渲染周期中的 state。
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.将处理程序中的输入 id 库化,减少传递和处理的道具。 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.将
defaultValue
传递给底层输入的defaultValue
。
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在咖喱处理程序中传递
id
<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>
);
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.