ENECHANGE Developer Blog

ENECHANGE開発者ブログ

最近遭遇した useEffect のアンチパターンを紹介 【React】

ENECHANGEの Yuto Ono です。エンジニアリングマネージャーをしながら、フロントエンドの開発もしています。

React を使った開発で悩むポイントの1つとしては、 useEffect の使い方だと思います。何でもできる魔法のようなフックですが、それゆえに、間違った使い方をされることも多い印象です。そこで今回は、最近遭遇した useEffect のアンチーパターンと、それの対処法を紹介していこうと思います。

こちらの公式ドキュメントに、さらに詳しく書いてありますので、詳しく知りたい方はこちらをご参照ください。

ja.react.dev

なお、最近のフロントエンド開発では TypeScript を使うことが多いですし、弊社でも TypeScript を使っていますが、今回は型についての話はしないので、 JavaScript でコードを記載しています。

useEffect のアンチパターンとしてよく取り上げられるのが、他の props や state から計算できるものを、新たな state を用意して useEffect の中で更新してしまう例です。

function Component({ firstName, lastName }) {
  // ダメ
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

こちらのコードが良くないのは、ぱっと見て気づく方も多いのではないでしょうか。なぜダメかというと、

  • この書き方だと余計なレンダリングが発生しパフォーマンスが悪化する
    • props が更新されたタイミングで1回目のレンダリング
    • 1回目のレンダリングで発火した useEffect 内の state 更新により2回目のレンダリング
  • コードが必要以上に複雑になり可読性が低下

これを改善すると下記のようなコードになります。

function Component({ firstName, lastName }) {
  // Good
  const fullName = firstName + ' ' + lastName;
  // ...
}

こちらの方がシンプルですし、パフォーマンスも良くなりますね。

この程度であれば、一定の実力のあるエンジニアはほとんどこのようなミスはしないのですが、複雑なパターンになると、意外と経験者でもアンチパターンで実装してしまうことがあることに最近気がつきました。

最近のコードレビューで指摘したケースは、簡単に書くと、以下のようなコードでした。

function Component({ firstName, lastName }) {
  // ダメ
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    if (firstName === '') {
      setFullName(lastName);
    } else {
      setFullName(firstName + ' ' + lastName);
    }
  }, [firstName, lastName]);
  // ...
}

確かに、条件分岐などのロジックが入って、1行で書けなくなると、つい useEffect を使いたくなる気持ちもわかりますし、実際のコードはこれよりももっと複雑だったりします。しかしよく考えると、これも他の props や state から計算できるという点で、最初に掲載したアンチーパターンと本質的には変わりません。これに対する自分なりの筋の良い対処法は2つあります(もちろん他にも方法はあると思います)。

1) fullNameを計算する関数を作る

function calcFullName(firstName, lastName) {
  if (firstName === '') {
    return lastName;
  }
  return firstName + ' ' + lastName;
}

function Component({ firstName, lastName }) {
  // Good
  const fullName = calcFullName(firstName, lastName);
  // ...
}

こっちの方がシンプルでコードの意図も伝わりやすくて良いですね! 僕もよく使う方法です。

2) useEffect の代わりに useMemo を使う

function Component({ firstName, lastName }) {
  // Good
  const fullName = useMemo(() => {
    if (firstName === '') {
      return lastName;
    }
    return firstName + ' ' + lastName;
  }, [firstName, lastName]);
  // ...
}

useMemo を使うことにより他の state や prop から値を計算しているという意図が伝わりやすく、良いですね。
また、レビュワー視点だと、 useEffect で書いてしまった人に useMemo で書き直してもらったときに1のやり方よりも変更が少なくなるのでレビュワーの認知負荷が下がるという効果もあります。実装者視点だと、関数名を考える必要がないというメリットもあります。というわけで、僕がレビューで指摘するときは、 useMemo の使用を提案することが多いです。

もしかしたら、わざわざ(計算量的に)メモ化する意味あるの?というツッコミが入りそうなので念のため補足しておくと、確かに公式ドキュメントにもある通り、よっぽど重い処理でなければ、メモ化による最適化の恩恵を受けることはありません。とはいえ、useEffect と違って余計なレンダリングが発生しない点や、可読性が向上するなどのメリットを考えると、十分に使う価値があるのではないかと思っています。また公式ドキュメントにも過度なメモ化を行うことによる重大な害はないとはっきり書いてあるので、迷ったら使うぐらいのスタンスで問題ないと思っています。

以上、最近見かけた useEffect のアンチーパターンとその対処法の解説でした。ぜひ今後の参考にしていただければ幸いです!