簡體   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