KENTEM TechBlog

建設業のDXを実現するKENTEMの技術ブログです。

【React19.2】新機能useEffectEventの使いどころと注意点

こんにちは!KENTEMのフロントエンジニア、S.W.です。 昨年React19の正式版が公開され、私のプロジェクトでは今年の夏にReact18から19へ更新を実施しました。

依存ライブラリの更新や移行作業には苦労しました…。

そして、更新直後の10月1日にReact19.2が公開されました。

すでにReact19.1へ更新済みなので恐れることなく、今回の開発でReact19.2を取り入れることにしました。

react.dev

早速、新機能 useEffectEvent の使いどころがあったので、使い方をご紹介します。

useEffectEventとは

簡単に言うとエフェクトの「イベント部分」を分離して定義することができるラップ関数です。 useEffectEvent は常に最新の props と state を参照できます。

呼び出しは次のフックからのみ可能です。

  • useEffect
  • useLayoutEffect
  • useInsertionEffect

どんなときに使えるの?

以下のようなコンポーネントを考えます(極端なパターンです)。

function TestTimer() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const [sum, setSum] = useState(0);

  const eventCount2 = useCallback(() => {
    console.log('eventCount2(useEffectTest)');
    if (count1 !== 1) return;
    setCount2(count1);
  }, [count1]); // ※

  useEffect(() => {
    console.log('useEffect count1(useEffectTest)');
    setCount1(1);
    const timer = setTimeout(() => {
      eventCount2();
    }, 300);
    return () => clearTimeout(timer);
  }, [eventCount2]); // ※

  useEffect(() => {
    console.log('useEffect sum(useEffectTest)');
    setSum(count1 + count2);
  }, [count1, count2]);

  return (
    <div>
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
      <div>sum: {sum}</div>
    </div>
  );
}

※ useEffect で呼び出している eventCount2 は、クロージャによって count1 が初回レンダリング時の状態のままになるため、依存配列に含めています。また、eventCount2 をそのまま依存配列に含めると、再レンダリング時に毎回再生成されて useEffect が頻発してしまうため、useCallback でラップし、依存配列に count1 を含めることで、count1 の更新時にのみ eventCount2 が再生成されるようにしています。

この例では、上記コンポーネントがレンダリングされるとき、以下の通り出力されます。

初回レンダリング時

  • useEffect count1(useEffectTest):1回
  • useEffect sum(useEffectTest):1回

状態変化時

  • useEffect count1(useEffectTest):1回
  • useEffect sum(useEffectTest):2回(count1変更時、count2変更時)

useEffect が計5回実行されています。これを以下の通り計3回になるよう改善します。

  • useEffect count1(useEffectTest) 初回レンダリング時のみ
  • useEffect sum(useEffectTest) 初回レンダリング時とcount2更新時のみ

useEffectEvent無しで改善

function TestTimer() {
  const [count1, setCount1] = useState(0);
  const count1Ref = useRef(count1);
  const [count2, setCount2] = useState(0);
  const [sum, setSum] = useState(0);

  const eventCount2 = () => {
    console.log('eventCount2(useEffectTest)');
    if (count1Ref.current !== 1) return;
    setCount2(1);
  };

  useEffect(() => {
    console.log('useEffect count1(useEffectTest)');
    setCount1(1);
    count1Ref.current = 1;
    const timer = setTimeout(() => {
      eventCount2();
    }, 300);
    return () => clearTimeout(timer);
  }, []);

  useEffect(() => {
    console.log('useEffect sum(useEffectTest)');
    if (!count2) return;
    setSum(count1 + count2);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [count2]);

  return (
    <div>
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
      <div>sum: {sum}</div>
    </div>
  );
}

目的は達成できたものの、ESlintの警告を抑制したり、ref に値を退避したりする実装はスマートとは言えません。

state を ref に保持する戦略や、依存配列からイベント関数を外す設計は煩雑さを招くことがあります。

このようなケース、似たような事例を見かけたことはありませんか?

私のプロジェクトのコードにも、そうした実装が含まれています…

それでは useEffectEvent を使ってみましょう!

useEffectEventで改善

function TestTimer() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const [sum, setSum] = useState(0);

  const eventCount2 = useEffectEvent(() => {
    console.log('eventCount2(useEffectTest)');
    if (count1 !== 1) return;
    setCount2(1);
  });

  useEffect(() => {
    console.log('useEffect count1(useEffectTest)');
    setCount1(1);
    const timer = setTimeout(() => {
      eventCount2();
    }, 300);
    return () => clearTimeout(timer);
  }, []);

  const eventSum = useEffectEvent((count: number) => {
    console.log('eventSum(useEffectTest)');
    setSum(count1 + count);
  });

  useEffect(() => {
    console.log('useEffect sum(useEffectTest)');
    if (!count2) return;
    eventSum(count2);
  }, [count2]);

  return (
    <div>
      <div>count1: {count1}</div>
      <div>count2: {count2}</div>
      <div>sum: {sum}</div>
    </div>
  );
}

useEffectEvent の利用により、依存関係と副作用の発火タイミングが明確になりました! ESlintの警告抑制や ref によるステータスの二重管理も無いのですっきりしています。

狙い通り、余計な副作用も発生していません。

最新の state 値を利用した計算も出来ています。

利用する際の注意

useEffectEvent でラップした関数は依存配列に含めない

常に最新の状態を保つため、頻繁に再生成されています。含めるべきではありません。

eslint-plugin-react-hooks@latestで制約がかけられています。

エフェクト内でのみ呼び出す

エフェクトの内部イベントとして定義・呼び出しを行い、他のコンポーネントやフックへ渡してはダメです。

eslint-plugin-react-hooks@latestで制約がかけられています。

useEffectEvent は、それを使用するエフェクトイベントのすぐ隣に宣言しましょう。

依存配列を避けるためのものではない

依存関係を誤魔化すとバグが隠蔽され、コードの理解が難しくなります。

依存値は明示的に列挙するか、必要に応じて ref を使って過去値と比較しましょう。

非リアクティブなロジックだけに使う

useEffectEvent は、値の変化に反応するリアクティブなロジックを抽出する用途には向いていません。

値の変化に依存する処理は他の手段で扱い、useEffectEvent は変化に依存しないロジックの抽出に限定してください。

useStateフックは非同期に状態を更新する

useEffectEvent は基本的には最新の props と state を参照しますが、setState の非同期更新によって、同一のレンダリングサイクル内での参照が古い値になることがあります。

最新値の保証が必要なケースでは、依存配列の適切な設定や ref の併用を検討してください。

参考リンク

まとめ

本稿では、useEffectEvent の活用と注意点を実践的に解説しました。依存関係を正しく扱い、非反応的なロジックはエフェクトから分離して設計することで、コードの読みやすさと保守性を高められます。

コード内の ESLint 警告の抑制を減らし、不要な再レンダリングの抑制にも寄与する可能性があります。

useEffectEvent の適切な使い分けとエフェクトの分離を、ぜひあなたのプロジェクトで実装してみてください。

おわりに

KENTEMでは、様々な拠点でエンジニアを大募集しています! 建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。

recruit.kentem.jp

career.kentem.jp