简体   繁体   English

关闭 `useState` 钩子的结果会导致烦人的行为

[英]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>
  );
};

Code sandbox 代码沙箱

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>
  );
}

Code Sandbox 代码沙箱

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>
  );
}

Code Sandbox 代码沙箱

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 更新,我认为您将实现您所追求的。

Issue问题

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"
}

Solution解决方案

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从这里建议只是优化

  1. 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 })); };
  2. 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> ); };
  3. 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演示

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

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.

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