Reactを基礎から学び直す #3: Hooksの基礎(useState/useEffect)と実践的な使い方

フリーランスでWeb制作をしている矢野です。

前回は、PropsとStateの役割の違いを整理しました。

今回は、Stateを実際に扱うためのHooks、useStateuseEffectの実践的な使い方を書きます。

RE:FITの開発中に、ESLintの新しいルールに引っかかって設計を見直した実話もあわせて紹介します。

useStateの基本と遅延初期化

useStateは、コンポーネントの中でStateを持つための最も基本的なHookです。

RE:FITの予約カレンダー画面では、こう使っています。

const [calendar] = useState<CalendarDay[]>(() =>
  generateCalendar(new Date())
);

ポイントは、初期値をgenerateCalendar(new Date())と直接書かず、() => generateCalendar(new Date())という関数(遅延初期化)にしていることです。

初期値をそのまま書くと、再レンダーのたびに計算が走ってしまいますが、関数で渡すと初回のレンダー時にしか実行されません。

カレンダーの生成のように多少コストのある処理を初期値にするときは、この書き方が安全だと学びました。

useEffectとは何か

useEffectは、レンダーが終わったあとに副作用(外部とのやり取り)を実行するためのHookです。

データの取得や、DOM操作、外部ライブラリとの連携などで使われます。

RE:FITでも最初、予約カレンダー画面でこのuseEffectを使っていました。

つまずいた話:ESLintに怒られた

もともとのBookingScreenは、こう書いていました。

BEFORE(最初に書いたコード)

const [calendar, setCalendar] = useState<CalendarDay[] | null>(null);

useEffect(() => {
  setCalendar(generateCalendar(new Date()));
}, []);

サーバー側でレンダーされる時刻とブラウザ側の時刻がズレると、日付ベースのカレンダー表示が食い違ってhydrationエラーになる可能性があります。

そこで、マウント後(=ブラウザ側)にだけカレンダーを生成する、という意図でこう書いていました。

ところがnpm run lintを実行すると、こんなエラーが出ました。

Calling setState() directly within an effect
react-hooks/set-state-in-effect

ESLintのreact-hooksプラグインが持つ比較的新しいルールで、「エフェクトの中で直接setStateを呼ぶと、レンダーの連鎖が起きて性能が落ちる可能性がある」という指摘でした。

なぜuseEffectを使わずに済んだか

エラーメッセージを読みながら考えて、そもそもこのアプリ全体はサーバーでレンダーする必要がないと気づきました。

RE:FITは診断から予約までを完結させるインタラクティブなミニアプリで、SEOのために事前レンダーしたいコンテンツがあるわけではありません。

そこで、アプリ全体を読み込む部分だけを、サーバーサイドレンダリングを無効化したコンポーネントに切り出しました。

AFTER

const DiagnosisApp = dynamic(() => import("./DiagnosisApp"), {
  ssr: false,
});
const [calendar] = useState<CalendarDay[]>(() =>
  generateCalendar(new Date())
);

サーバーで描画されなくなったことで、hydrationのズレという前提条件自体がなくなり、useEffectを使わずに冒頭で紹介した遅延初期化だけで安全に書けるようになりました。

useEffectを使わずに済ませる方法を探した結果、コードもシンプルになったので、結果的に良い設計変更になったと思っています。

Hooksを使い分ける基準

この経験から、自分の中でのHooksの使い分け基準が整理できました。

初期値の計算やStateの保持はuseState(重い処理は遅延初期化で)、外部システムとの同期がどうしても必要なときだけuseEffect、という順番で考えるようにしています。

「なんとなくuseEffectで書けば動く」ではなく、まず本当にuseEffectが必要かを疑う、というのが今回いちばんの学びでした。

まとめ

今回は、useStateの遅延初期化と、useEffectまわりで実際につまずいた経験を書きました。

ESLintの新しいルールに指摘されたことがきっかけで、アプリ全体の設計を見直すことになったのは、自分でも意外な展開でした。

次回は、複数の画面をまたぐStateをどう管理するか、RE:FITで採用したuseReducerを中心に整理します。