React Hooks を使っている人なら必ず使うuseEffect
。useEffect
はとても扱いが難しく、深く考えずに使ってしまうと思わぬバグを生んでしまったり、コードの変更を行うのが難くなってしまう。
本記事は、上記のような扱いの難しいuseEffect
の使い方をさっとみることができるように短くまとめたものである。
この記事は主に以下の2つの記事を参考に構成されている。
基本的な使い方
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
で使う場合は、注意深く実装する必要がある。
悪い例
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
に依存しているのはoutputEvenNumber
とcount
である。
上記の問題を解決するためには以下のように書く。
良い例
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のスコープ内にある値を追加した時に依存関係が明確になる - さらに、
outputEvenNumber
をuseEffect
の中に書くことで、関数がuseEffect
の第2引数に指定されている値以外の影響を受けていないことを保証できる。
propsやstateに依存する関数をComponent内で共通で使用する
良い例
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に依存しない関数を使用する
良い例
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
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
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
がレンダリングされる
良い例
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
例えば、他の変数によって変化をつけたいときはどのようにすれば良いだろうか
次の例では秒数を指定する方法を書いた
良い例
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のコメントは他にもあるので詳しくは公式ドキュメントを参照してください)
以上です!間違っている点や、アドバイスなどがございましたら、コメントにて指摘していただければ幸いです。
ありがとうございました。