[英]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.