🐃

React.useEffectの分かりにくさをなんとかしたい

2021/11/24に公開
1

useEffect(() => ..., [])って書いてあっても、ぱっと見どんな役割なのか分かりづらいですよね。コードの可読性が上がるようにちょっとした工夫を考えてみました。

(2021.11.27) react-useを追記しました。

useEffectが分からない

U-motion開発部ではフロントエンド開発に使用するライブラリをAngularJS → Reactに移行しました。大きな規模のコードを複数人で開発することになるため、勉強会を開いて書き方のキャッチアップやよりよい手法の共有を行っています。そのなかで説明に結構困るのがReact.useEffectです。

https://ja.reactjs.org/docs/hooks-effect.html

副作用 (effect) フック により、関数コンポーネント内で副作用を実行することができるようになります

もうこの時点で何を言っているのか分からない感じもするわけですが、ReactのuseEffectにはいくつか困った点があります。

  • 書き方が覚えにくい。
    • ウォッチ対象の状態の指定が一般的ではない(第2引数にArrayで渡す)
  • useEffectの数が増えてくると、それぞれの目的が一目で分からない(マウントしたタイミングだけ?特定の状態が変わったとき?)
  • しかし、どうしても必要になる

Reactが初めての人に説明するときに「useEffectの第2引数に空のArrayをセットすると、コンポーネントをマウントしたときに一度だけ第1引数のハンドラが実行されます」って言うんですけど、言ってる自分でも何を言ってるんだ?というAPIです。

さらにようやく書き方を覚えても、コンポーネントが複雑になって useEffect がいくつも記述されていくとどれがどの副作用なのか「分からない」状態が増えがちです。

チームでの開発など他人の書いたコンポーネントのコードを読むときは特に問題になりそうで、なんとかしたいものです。

対処法1: 第一引数に渡すハンドラに名前をつける

ハンドラに無名関数を使うのをやめてみます。

useEffect(function onDidMount() {
  ...
}, []);

いちど変数に代入すると useEffect() の記述がさらにすっきりして第2引数が見やすくなります。

const [counter, setCounter] = useState(0);

const onDidMount = () => {
  ...
};

const onDidUpdate = () => {
  ...
};

useEffect(onDidMount, []);
useEffect(onDidUpdate, [counter]);

チーム内でケースごとにつけるハンドラの名前をルール化しておくと、学習コストを下げてコードの可読性をあげることができそうです。また、書き方の工夫だけで対応できるのもシンプルです。
ただし、onDidUpdateが複数になる場合は、onCounterDidUpdate、 onShouldShowCounterDidUpdate などハンドラの名前をユニークなものにしていく必要があり、これはこれで混乱しそうです。

対処法2: カスタム・フックでラップする

カスタム・フックでuseEffectをラップして分かりやすい名前をつけるという方法もあります。こちらは探してみたらNPMパッケージがいくつか公開されていました。役割がはっきりしていて最初に学ぶ際のコストが低く、またコードの書き方を強制できる点ではハンドラに名前をつけることをルール化するより効果が大きそうです。

react-use-lifecycle-helpers

https://github.com/mohcinenazrhan/react-use-lifecycle-helpers

import useLifecycleMethods from "react-use-lifecycle-helpers";

export default function MyComponent() {
  const [state, setState] = useState({ count: 0, bool: false });

  const { useDidMount } = useLifecycleMethods();
  const { useDidUpdate } = useLifecycleMethods(state, props);

  useDidMount(() => {
    console.log("MyComponent is mounted");
  });
  
  useDidUpdate(() => {
    console.log("MyComponent is updated");
  });
}

react lifecycle hooks

https://github.com/erkobridee/react-lifecycle-hooks

import { useConstructor } from '@erkobridee/react-lifecycle-hooks';

export const Component = () => {
  useConstructor(() => console.log(`executes before mount the component`));

  return <div>Component</div>;
};

export default Component;
import { useDidUpdate } from '@erkobridee/react-lifecycle-hooks';

export const Component = () => {
  const [count, setCount] = React.useState(0);

  useDidUpdate(() => console.log(`executes whenever the component updates`));

  useDidUpdate(() => console.log(`count value updated to ${count}`), [count]);
}

@better-typed/react-lifecycle-hooks

https://github.com/BetterTyped/react-lifecycle-hooks

import React from "react";
import { useDidMount, useDidRender, useDidUpdate, useWillUnmount } from "@better-typed/react-lifecycle-hooks";

const MyComponent: React.FC = () => {
  const [isOpen, setIsOpen] = React.useState(false)

  // Called first
  useDidMount(() => {
    // ...
  })

  // Called second, when initial DOM changes are triggered
  useDidRender(() => {
    // ...
  })

  // Called when isOpen change
  useDidUpdate(() => {
    // ...
  }, [isOpen])
  
  ...
  
}

react-use

https://github.com/streamich/react-use

react-use の実装から学ぶ custom hooksを参考にしました。

useUpdateEffect

import React from 'react'
import {useUpdateEffect} from 'react-use';

const Demo = () => {
  const [count, setCount] = React.useState(0);
  
  React.useEffect(() => {
    const interval = setInterval(() => {
      setCount(count => count + 1)
    }, 1000)
    
    return () => {
      clearInterval(interval)
    }
  }, [])
  
  useUpdateEffect(() => {
    console.log('count', count) // will only show 1 and beyond
    
    return () => { // *OPTIONAL*
      // do something on unmount
    }
  }) // you can include deps array if necessary

  return <div>Count: {count}</div>
};

マウント時の初期化でのステート更新は無視して「変更」のときだけハンドラを呼んでくれるのが便利ですね。
https://github.com/streamich/react-use/blob/master/docs/useUpdateEffect.md

そのほかに useMountuseUnmount があります。

まとめ

  • そもそも必要な機能をuseEffectでどう書くのか分かりにくい
  • useEffectで記述されたコードが分かりにくい

2つの問題について対策を考えてみました。この問題、ほかの組織でどのような対応をしているのか気になります。良い方法があったらコメントで教えてください。

U-motion開発部によるその他の記事

はじめてこのブログを読んだ方へ: U-motionとは?

U-motionは牛の首につけたセンサーを使って活動内容を記録、AIの力で健康状態を解析して畜産農家さんをサポートするモニタリング・システムです。

U-motion開発部は、

  • センサーから集めたデータを処理する Rails/Pythonベースのバックエンド・システム
  • データをグラフ化したり牛群を管理したりできるReactベースのウェブ・アプリケーション
  • 牛の健康状態の変化をプッシュ通知のアラートでお知らせするReact Nativeベースのスマホ・アプリケーション

などを開発中。こちらのアカウントでは、開発時に得たチーム内の知見を公開しています。

https://www.desamis.co.jp/product/

https://www.youtube.com/watch?v=6Bw1wXX0i3s

Discussion

Yuto OnoYuto Ono

useEffect の可読性の悪さに悩んでいたので、参考にします!
ありがとうございます!