変なところでuseEffectを使わない術
「変なところでuseEffectを使う」とは、「useEffectを単なるsteteの同期の役割のために使う」ことを指します。
みなさん、ReactのuseEffectを正しく使えてる自信がありますか?
私はReactエンジニアとして開発の現場に携わっていますが、useEffectを単なるsteteの同期の役割のために使うパターンがまだ多いなぁと感じてます。
そのようにuseEffectを扱うと、バグが発生しやすく、理解しづらいコードにもつながり、メンテナンス性も悪くなります。
この記事を読むことによって、useEffectの良くない書き方を学んで、どのようにコードを書いていけば良いかが分かるようになれれば幸いです。
stateの同期のためだけにuseEffectを使うとは
まず、「stateの同期のためだけにuseEffectを使う」とは、そのuseEffect内で行っていることがuseStateのsetState関数の実行のみであることを言います。
ブログの記事一覧ページに記事が10個以上あればページナビゲーションを配置する際を例に考えてみます。
const PostList: FC = () => {
// useData()は引数のURLにGETリクエストを送りデータを取得するカスタムフック
const data = useData("https://hogehoge.com/posts");
return (
<div>
<div>記事一覧</div>
<div>
{data.posts.map((post: any) => (
<div>{post.title}</div>
))}
</div>
<Pagenation length={data?.items.length} />
</div>
);
};
const Pagenation: FC<{ length: number }> = ({ length }) => {
const [showPagenation, setShowPagenation] = useState(false);
// ↓↓↓↓↓↓↓ これです ↓↓↓↓↓↓↓
// stateの同期のためのuseEffect
useEffect(() => {
if (length > 10) {
setShowPagenation(true);
return;
}
setShowPagenation(false);
}, [length]);
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
return <div>{showPagenation && <div>Pagenation...</div>}</div>;
};
この例のPagenationコンポーネントで実装しているuseEffectがまさにstateの同期のためだけにuseEffectを使っている例です。
useEffectの第二引数にstateを入れて、その変更に伴って別のstateも更新するような処理です。
なぜ stateの同期のためだけにuseEffectを 使うことがいけないのか
それは、useEffectは外部システムを扱うときに処理を書く場所だからです。
外部システムを扱うとは以下のようなことを指します
- APIを使ってデータのフェッチを行う
- ブラウザーのAPIを使って、特定の要素までのスクロールを行ったり、Input要素にフォーカスを当てる
- リアルタイムチャット機能などで使われる、サーバーとのリアルタイム接続の開始・終了する
- setInterval や setTimeout を使って時間を扱う処理を行う
- etc
(もしよくわからない場合は
「ReactはあくまでUIライブラリであり、UI構築とは異なることをしていたら外部システムを扱ってる」と考えてもいいかもしれません。)
上記のuseEffectの原則に則らずにstateの変更に伴って別のstateを更新(同期)させるために使うと一気にメンテナンス性が悪くなるのも理由の一つです。
useEffectの原則に則らずに書いたコードが先ほどのPagenationコンポーネントです。
useEffectに第二引数を入れるとその引数の値が変更されたらuseEffectが発火されることはReactエンジニアなら理解してることだと思いますが、実際useEffectがいつ発火するのかをすべて覚えて実装しているエンジニアはいないと思います。
これはつまり、以下のようなコードを書いても、実際に画面上のどういう操作を行うとuseEffectが発火されるかを覚えられる人はいないということです。
useEffect(() => {
// do something
}, [stateA, stateB, stateC])
故に後からこのコンポーネントに改修を加えようとする際に、useEffectがどういうケースのことを担保してるのかがはっきりと伝わらないので、どこに手を付けるのがベストなのかが分かりづらくなるのです。少なくともuseEffectが何をやってるのか、このuseEffectに影響ないように修正をするにはどうすればいいのかを理解して作業に取り掛かる必要があり大変面倒です。。。
(説明が下手ですみません。経験したことのある方ならわかってくださると信じてます。)
ではどのように実装するのがよいのかの改善例をいくつか紹介します。
改善例1 レンダリングの最中に計算する
最初に紹介したようなコードを書く人は少ないと思いますが、今回の実装に関しては以下のように書けば不要なuseEffectを省けます。
const Pagenation: FC<{ length: number }> = ({ length }) => {
const showPagenation = length > 10; // わざわざ`state`を使わずに計算して扱う
return <div>{showPagenation && <div>Pagenation...</div>}</div>;
};
改善例2 ステートの更新は同じイベントハンドラで定義する
useEffectを以下のように使うコードもたまに見ますが、シンプルに同じイベントハンドラでsetStateの実行を行えばよい場合もあります。
// countAとcountBがあり、countAが変更されたらcountBをAの2倍にする
const SampleNG: FC = () => {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
useEffect(() => {
setCountB(countA * 2);
}, [countA]);
const handleClick = () => {
setCountA(countA + 1);
};
return (
<>
<div>{countA}</div>
<div>{countB}</div>
<button onClick={handleClick}>button</button>
</>
);
};
同じイベントハンドラ内で処理を書くとこうなる↓
const SampleOK: FC = () => {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
const handleClick = () => {
const newCountA = countA + 1;
setCountA(newCountA);
setCountB(newCountA * 2);
};
return (
<>
<div>{countA}</div>
<div>{countB}</div>
<button onClick={handleClick}>button</button>
</>
);
};
stateが親からprops経由で渡ってくるときも同様です。その時は親で一緒にstateを更新する処理を書きましょう💡
なぜ自分はuseEffectを正しく扱えなかったのか
今回、useEffectをstateの同期のために使わないでほしい旨を書きましたが、どうしてそのようなコードを書くことになるのかを考えてみました。私が考える限りでは以下の理由が思い浮かびます。
-
Reactのレンダリングの順番や発火条件、setStateでの更新とは何かを正しく理解していない
→ よくわかってないから脳死でuseEffectを使い、楽にstateの同期をしようと考える。(実際には茨に道に進んでます...) -
コンポーネントごとに責任を分割したくて、コンポーネントごとに
stateを管理してしまっている
→ 単一責任とやらを変に取り込もうとするとこうなります。(過去の自分)
コンテイナーコンポーネントや、Solid原則を理解してReactコンポーネントをデザインする知識をつけることがおすすめです。
https://zenn.dev/morinokami/books/learning-patterns-1/viewer/presentational-container-pattern
https://zenn.dev/koki_tech/articles/361bb8f2278764 -
ライブラリなどの公式ドキュメントでも
useEffectをstateの同期のために使っている例があり、正しい使い方と思い込んでしまう
→ 本当に他のやり方がないか考えて、どうしてもだめな場合に公式のコードを扱いましょう。ただし、扱うときはuseEffectを使わないといけない理由をカスタムフックに隠ぺいすること前提となります!!
自分は上記のような理由で間違ってuseEffectを使っていて、開発の際に苦戦したことがありました。
そして一番勉強になるサイトはReactの新しい公式サイトです。まだ読んでない方は是非一読してみてください。新しい学びがきっと得られるはずです。
公式サイトは2023年3月に一新されて、Function Componentでの解説、図解も多く、Reactの良い使い方や悪い使い方の例もたくさん紹介されており、今のエンジニアにとっつきやすいです。
最後に
結局はReactエンジニアなので、Reactのことを深く理解して扱うことが良い実装につながると思ってます。自分もまだまだ勉強が足りてないので、もっと勉強しようと思いました。
もしこの記事で何か質問がある場合や、間違ってるなどの指摘がありましたらコメント欄にて頂けると幸いです。
Discussion
失礼します。
の部分については、以下の章がドンピシャな解説になっていると思います。
あくまで、React 特有の Props, State に関する技術的な制約が第一であり、SOLID やコンテナ・プレゼンテーション分離といったものは、その制約の上に実装するテクニックと捉えると良さそうです。
コメントありがとうございます!
ご指摘の通り、SOLID原則やコンテナ・プレゼンテーションの分離は、Reactの技術的な制約の上で実装するテクニックと捉えるべきだと感じました。今後もこの点に注意しながら、より効果的なコンポーネント設計について学んでいこうと思います。