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

Code sandbox

但这并不符合我们的要求。每个 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>
  );
}

Code Sandbox

这行不通。似乎所有更改处理程序都在 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>
  );
}

Code Sandbox

这确实有效。但是这样使用refs让我有点不安。

实现这样的目标的标准方法是什么?

如果您举第一个示例并简单地使用功能状态更新,我认为您将实现您所追求的目标。

问题

通过非功能性更新,所有输入同时挂载并调用它们的 onChange 处理程序。 handleChange 回调使用它所包含的渲染周期中的状态。当所有输入在同一渲染周期中排队更新时,每个后续更新都会覆盖先前的状态更新。更新状态的最后一个输入是“获胜”的输入。这就是为什么您会看到:

{
  "5": "five"
}

而不是

{
  "1": "one",
  "2": "two",
  "3": "three",
  "4": "four",
  "5": "five"
}

解决方案

使用功能状态更新从之前的状态正确更新,而不是之前渲染周期的状态。

setFormState(formState => ({ ...formState, [id]: value }));

这里的建议只是优化

  1. Curry 处理程序中的输入 id,减少了一个传递和处理的 props。子输入也不需要知道它来处理变化。

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