LoginSignup
272
183

More than 3 years have passed since last update.

React Hooks の useEffect の正しい使い方

Last updated at Posted at 2019-12-20

React Hooks を使っている人なら必ず使うuseEffectuseEffectはとても扱いが難しく、深く考えずに使ってしまうと思わぬバグを生んでしまったり、コードの変更を行うのが難くなってしまう。
本記事は、上記のような扱いの難しいuseEffectの使い方をさっとみることができるように短くまとめたものである。

この記事は主に以下の2つの記事を参考に構成されている。

基本的な使い方

Counter.js

const Counter = () => {
    const [ count, setCount ] = useState(0);

    /* 
  * useEffectは、第一引数にcallbackを入れて、第二引数に依存する値の配列を入れる
  * 依存する値が変更される度にcallbackが実行される
  */
    useEffect(
        () => {
            console.log(count);
        },
        [ count ]
    );

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>カウント</button>
        </div>
    );
};


propsやstateに依存する関数を使用する

関数をuseEffectで使う場合は、注意深く実装する必要がある。

悪い例

Counter.js

const Counter = () => {
    const [ count, setCount ] = useState(0);

    // 偶数の時にlogを出力したい
    const outputEvenNumber = () => {
        if (count % 2 === 0) {
            console.log(count);
        }
    };

    useEffect(
        () => {
            outputEvenNumber();
        },
        [ outputEvenNumber ]
    );

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>カウント</button>
        </div>
    );
};

この例のよくない点

  • 現段階ではcountが更新される度にoutputEvenNumberが再生成されているため正常に動いている。
  • しかし、useEffectの第2引数にoutputEvenNumberを入れていることで、もしoutputEvenNumberに新しくComponentのスコープ内にある値を追加したいとなった時に正しく動かない可能性がある。
  • つまりここで、useEffectに依存しているのはoutputEvenNumbercountである。

上記の問題を解決するためには以下のように書く。

良い例

Counter.js

const Counter = () => {
    const [ count, setCount ] = useState(0);

    useEffect(
        () => {
            // 偶数の時にlogを出力したい
            const outputEvenNumber = () => {
                if (count % 2 === 0) {
                    console.log(count);
                }
            };
            outputEvenNumber();
        },
        [ count ]
    );

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>カウント</button>
        </div>
    );
};

改善した点

  • 関数をuseEffectの中に入れて,外部から依存している変数をcountのみにした。
  • こうすることで、outputEvenNumberに新しくComponentのスコープ内にある値を追加した時に依存関係が明確になる
  • さらに、outputEvenNumberuseEffectの中に書くことで、関数がuseEffectの第2引数に指定されている値以外の影響を受けていないことを保証できる。

propsやstateに依存する関数をComponent内で共通で使用する

良い例

Counter.js

const Counter = () => {
    const [ count, setCount ] = useState(0);

    /*
  * useCallback は第2引数に指定した値が変化した時にのみ関数を再生成するHookです
  */
    const outputEvenNumber = useCallback(
        () => {
            if (count % 2 === 0) {
                console.log(count);
            }
        },
        [ count ]
    );

    useEffect(
        () => {
            outputEvenNumber();
            // Do something
        },
        [ outputEvenNumber ]
    );

    useEffect(
        () => {
            outputEvenNumber();
            // Do something
        },
        [ outputEvenNumber ]
    );

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>カウント</button>
        </div>
    );
};


良い点

  • useCallbackを使ってcountが依存していることを明確にしている
  • 他の変数を加えた時に、useCallbackの第2引数に値を追加すれば良いだけなので安全である

propsやstateに依存しない関数を使用する

良い例

Counter.js

const outputEvenNumber = (count) => {
    if (count % 2 === 0) {
        console.log(count);
    }
};

const Counter = () => {
    const [ count, setCount ] = useState(0);

    useEffect(
        () => {
            outputEvenNumber(count);
            // Do something
        },
        [ count ]
    );

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>カウント</button>
        </div>
    );
};

良い点

  • この場合は単純にComponentの外に定義してあげれば良い
  • 中に定義するとしたらuseCallbackを使って第2引数に[]を指定すれば良いが、外に出した方が無駄な処理も減り、読みやすくなる

useEffectの中でstateを更新したいとき 1

カウントアップをするようなUIを実現したい時を例にあげる

悪い例 1

Counter.js

const Counter = () => {
    const [ count, setCount ] = useState(0);

    useEffect(
        () => {
            const time = setInterval(() => {
                setCount(count + 1);
            }, 1000);

            return () => clearInterval(time);
        },
        [ count, setCount ]
    );

    return (
        <div>
            <p>{count}</p>
        </div>
    );
};


悪い点

  • stateが更新される度にsetIntervalを呼び出してしまっている

悪い例 2

Counter.js

const Counter = () => {
    const [ count, setCount ] = useState(0);

    useEffect(() => {
        const time = setInterval(() => {
            setCount(count + 1);
        }, 1000);

        return () => clearInterval(time);
    },[]);

    return (
        <div>
            <p>{count}</p>
        </div>
    );
};


悪い点

  • count変数が変化しないために、カウントアップされない
  • useEffect内のcount変数が更新されていないため、常にcount === 0である
  • そのため、常に1がレンダリングされる

良い例

Counter.js

const Counter = () => {
    const [ count, setCount ] = useState(0);

    useEffect(() => {
        const time = setInterval(() => {
            setCount(prevCount => prevCount + 1);
        }, 1000);

        return () => clearInterval(time);
    }, []);

    return (
        <div>
            <p>{count}</p>
        </div>
    );
};

良い点

  • 新しいstateがsetCount関数に、引数として渡ってきているため、カウントアップに成功する

useEffectの中でstateを更新したいとき 2

例えば、他の変数によって変化をつけたいときはどのようにすれば良いだろうか
次の例では秒数を指定する方法を書いた

良い例

Counter.js

const reducer = (state, action) => {
    switch (action.type) {
        case "SET_TICK": {
            return {
               ...state,
               count: 0,
               tick: action.tick
            };
        }
        case "ADD_COUNT": {
            return {
                ...state,
                count: state.count + 1
            };
        }
        default:
            return state;
    }
};

const Counter = () => {
    const [ state, dispatch ] = useReducer(reducer, { tick: 1000, count: 0 });

    const handleOnChangeTick = (e) => {
        dispatch({ type: "SET_TICK", tick: parseInt(e.target.value) });
    };

    useEffect(
        () => {
            const time = setInterval(() => {
                dispatch({ type: "ADD_COUNT" });
            }, state.tick);

            return () => clearInterval(time);
        },
        [ state.tick ]
    );

    return (
        <div>
            <p>{state.count}</p>
            <input value={state.tick} onChange={handleOnChangeTick} />
        </div>
    );
}

良い点

  • useReducerを使うことで、複数の変数に対しても無駄な変数をuseEffectの第2引数に入れることなく、実現できた
  • また、dispatch関数は常に最新のstateが共有されるのでreducerを見れば依存関係がすぐにわかる
  • propsに依存する場合は、reducer関数をComponentの中で宣言するようにすれば良い

バグを生まないために

バグを生まないために大切なのは静的にコードを解析して、バグの温床となるコードを予め潰すことです。これを行ってくれる一般的なツールが ESLint です。幸い、eslint-plugin-react-hooks という React の Hooks でも Lint を行ってくれる Plugin がすでに用意されているのでそれを使うとよりバグを減らすことができます。

この Plugin は useEffect の第2引数に指定している変数の数が足りていない時にエラーを出してくれるものです。

しかし、このプラグインも完璧ではなく、この Lint の指示通りに修正したとしてもバグが起こることもあります。そのため、とりあえずは Lint に従い、意図した挙動になっていない時にはコメントで// eslint-disable-next-line react-hooks/exhaustive-depsのように書くと、コメントの次の行のコードの Lint を無効にしてくれます。(ESLintのコメントは他にもあるので詳しくは公式ドキュメントを参照してください)

以上です!間違っている点や、アドバイスなどがございましたら、コメントにて指摘していただければ幸いです。
ありがとうございました。

272
183
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
272
183