[英]Closure on the result of a `useState` hook causes annoying behaviour
我有一个这样的组件:
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>
);
};
我希望它表现得像一个不受控制的组件,但我希望它有一个我们可以设置的初始值。
在我的父组件中,这工作正常,
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>
);
}
但问题是 formState 的值在MyInputs
onchange 事件之前不会反映formState
上的值。
好吧,我可以在 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>
);
};
但这并不符合我们的要求。 每个 MyInputs 在首次挂载时都会调用 onChange,但在每种情况下,对主 onChange 处理程序的formState
的引用都是陈旧的。
IE。 页面首次加载时的 console.log output 是:
1 one {}
2 two {}
3 three {}
4 four {}
5 five {}
所以我认为使用 ref 可以解决这个问题。 例如:
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>
);
}
这行不通。 似乎所有更改处理程序都会在 formState 上的 useEffect 有机会更新 ref之前触发。
1 one {} {"current":{}}
2 two {} {"current":{}}
3 three {} {"current":{}}
4 four {} {"current":{}}
5 five {} {"current":{}}
我想我可以直接改变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>
);
}
这确实有效。 但是这种对 refs 的使用让我有点不安。
实现这样的目标的标准方法是什么?
如果您举第一个示例并简单地使用功能性 state 更新,我认为您将实现您所追求的。
通过非功能性更新,所有输入同时挂载并调用它们的onChange
处理程序。 handleChange
回调使用包含在其中的渲染周期中的 state。当所有输入在同一渲染周期中排队更新时,每个后续更新都会覆盖先前的 state 更新。 更新 state 的最后一个输入是“获胜”的输入。 这就是为什么您会看到:
{
"5": "five"
}
代替
{
"1": "one",
"2": "two",
"3": "three",
"4": "four",
"5": "five"
}
使用功能性 state 更新以正确更新前一个 state,而不是前一个渲染周期中的 state。
setFormState(formState => ({ ...formState, [id]: value }));
从这里建议只是优化
将处理程序中的输入 id 库化,减少传递和处理的道具。 孩子们的输入也不需要知道它来处理变化。
const handleChange = (id) => (value) => { setFormState((formState) => ({...formState, [id]: value })); };
将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> ); };
在咖喱处理程序中传递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)} />
演示代码
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.