変なところで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の技術的な制約の上で実装するテクニックと捉えるべきだと感じました。今後もこの点に注意しながら、より効果的なコンポーネント設計について学んでいこうと思います。