繁体   English   中英

关闭 `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 的值在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 }));

从这里建议只是优化

  1. 将处理程序中的输入 id 库化,减少传递和处理的道具。 孩子们的输入也不需要知道它来处理变化。

     const handleChange = (id) => (value) => { setFormState((formState) => ({...formState, [id]: value })); };
  2. 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> ); };
  3. 在咖喱处理程序中传递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)} />

演示

编辑closure-on-the-result-of-a-usestate-hook-causes-annoying-behaviour

演示代码

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM