关闭 useState 钩子的结果会导致烦人的行为
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
的值在触发 onchange 事件之前不会反映 MyInputs
上的值。
好吧,我可以在 MyInput 上添加一个 'component did mount' 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
都是陈旧的。
即。页面首次加载时的 console.log 输出是:
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>
);
}
这行不通。似乎所有更改处理程序都在 before useEffect on formState 有机会更新 ref.
1 one {} {"current":{}}
2 two {} {"current":{}}
3 three {} {"current":{}}
4 four {} {"current":{}}
5 five {} {"current":{}}
我想我可以直接改变 handleChange 函数中的 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让我有点不安。
实现这样的目标的标准方法是什么?
如果您举第一个示例并简单地使用功能状态更新,我认为您将实现您所追求的目标。
问题
通过非功能性更新,所有输入同时挂载并调用它们的 onChange
处理程序。 handleChange
回调使用它所包含的渲染周期中的状态。当所有输入在同一渲染周期中排队更新时,每个后续更新都会覆盖先前的状态更新。更新状态的最后一个输入是“获胜”的输入。这就是为什么您会看到:
{
"5": "five"
}
而不是
{
"1": "one",
"2": "two",
"3": "three",
"4": "four",
"5": "five"
}
解决方案
使用功能状态更新从之前的状态正确更新,而不是之前渲染周期的状态。
setFormState(formState => ({ ...formState, [id]: value }));
这里的建议只是优化
Curry 处理程序中的输入 id,减少了一个传递和处理的 props。子输入也不需要知道它来处理变化。
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>
);
};
在 curried 处理程序中传递 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>
);
}
我有这样一个组件:
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
的值在触发 onchange 事件之前不会反映 MyInputs
上的值。
好吧,我可以在 MyInput 上添加一个 'component did mount' 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
都是陈旧的。
即。页面首次加载时的 console.log 输出是:
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>
);
}
这行不通。似乎所有更改处理程序都在 before useEffect on formState 有机会更新 ref.
1 one {} {"current":{}}
2 two {} {"current":{}}
3 three {} {"current":{}}
4 four {} {"current":{}}
5 five {} {"current":{}}
我想我可以直接改变 handleChange 函数中的 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让我有点不安。
实现这样的目标的标准方法是什么?
如果您举第一个示例并简单地使用功能状态更新,我认为您将实现您所追求的目标。
问题
通过非功能性更新,所有输入同时挂载并调用它们的 onChange
处理程序。 handleChange
回调使用它所包含的渲染周期中的状态。当所有输入在同一渲染周期中排队更新时,每个后续更新都会覆盖先前的状态更新。更新状态的最后一个输入是“获胜”的输入。这就是为什么您会看到:
{
"5": "five"
}
而不是
{
"1": "one",
"2": "two",
"3": "three",
"4": "four",
"5": "five"
}
解决方案
使用功能状态更新从之前的状态正确更新,而不是之前渲染周期的状态。
setFormState(formState => ({ ...formState, [id]: value }));
这里的建议只是优化
Curry 处理程序中的输入 id,减少了一个传递和处理的 props。子输入也不需要知道它来处理变化。
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> ); };
在 curried 处理程序中传递
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>
);
}