かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React Hooks useState state の更新にハマる

useState の setter で値を更新しても即時 state に反映されるわけではないので、更新された state を使って別の処理を行いたい時などで意図しない動作になってしまうことがあります。

🐞

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  const updateSquare = () => {
    setSquare( count * count );
  }

  const onCountup = () => {
    setCount( count + 1 );
    // ここではまだ count は更新されているとは限らない
    updateSquare();
  }
 
  return (
    <>
      <p>Count: {count}</p>
      <p>Square: {square}</p>
      <button onClick={onCountup}>+1</button>
    </>
  );
}

👍

useState の setter で更新されたことを期待する state を使うのではなく、更新する値を別途変数化してそれを使ってそれぞれを更新すればOK

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  const updateSquare = ( val ) => {
    setSquare( val * val );
  }

  const onCountup = () => {
    const newCount = count + 1; 
    setCount( newCount );
    updateSquare( newCount );
  }
 
  return (
    <>
      <p>Count: {count}</p>
      <p>Square: {square}</p>
      <button onClick={onCountup}>+1</button>
    </>
  );
}

👍👍Functional updates (高階関数)を使う

useState の setter に関数を渡す事ができ、この関数は前回の state を引数で受取り、返した値が新しい state になる
cf. フック API リファレンス 関数型の更新 – React

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  const updateSquare = ( val ) => {
    setSquare( val * val );
  }

  const onCountup = () => {
    setCount( prevCount => {
      const newCount = prevCount + 1; 
      updateSquare( newCount )
      return newCount;
    } );
  }
 
  return (
    <>
      <p>Count: {count}</p>
      <p>Square: {square}</p>
      <button onClick={onCountup}>+1</button>
    </>
  );
}

Functional updates (高階関数)を使うメリット

useCallback で関数の再生産を行わないようにする際に、高階関数を使う方法だと依存 (deps) を無くせるのでパフォーマンスを良くすることができる

🤔Functional updates (高階関数)でない方法の場合

Functional updates を使わない場合 state を deps に含める必要があるので、 state が変更される度に関数が再定義される

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  // ...
  
  // 🤔 `count` が変更される度に `onCountup` は再定義される
  const onCountup = useCallback(() => {
    const newCount = count + 1; 
    setCount( newCount );
    updateSquare( newCount );
  }, [count]);
 
  return ( ... );
}
🐞依存を与えないと逆にバグを発生させる

deps を空配列 [] にすれば依存はなくなり、関数の生成は1度になるが、関数内で使っている state が関数生成時の値でメモ化されてしまうので意図した動作にならない

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  // …

  // 🐞 `count` は初期値 initCount で固定されしまう
  const onCountup = useCallback(() => {
    const newCount = count + 1; 
    setCount( newCount );
    updateSquare( newCount );
  }, []);

  return ( ... );
}

👍👍👍Functional updates (高階関数)を使う方法の場合

Functional updates を利用すると関数外の変数を使わないので deps を無くせるので、関数の生成は1度で関数内の処理も意図したとおりに動作させることができる

function Counter({ initCount }) {
  const [count, setCount] = useState(initCount);
  const [square, setSquare] = useState(initCount * initCount);
  
  // …

  // 👍関数外の変数を使わないので関数再定義の依存がない
  const onCountup = useCallback(() => {
    setCount( prevCount => {
      const newCount = prevCount + 1; 
      updateSquare( newCount )
      return newCount;
    } ), []);
  }
 
  return ( ... );
}

sample

See the Pen React Hooks useState setter by KIKIKI (@chaika-design) on CodePen.

所感

Functional updates 知らなかった…
Array.map(function(val, key)) みないな感じなので、高階関数って呼んで良いんだと思うけどチョット自信ない。

codepen でサクット React Hooks も試せるから挙動確認みたいな時に便利ですね


[参考]