Reactキャッチアップへの道① 『useStateとuseEffect』
こんにちは!
スマレジ テックファームのMichiです!
前回の記事『Webエンジニアへ転職して1年が経ちました』で書いた通り、Q1(5月~7月)は「React/TypeScriptの習得」を目指します。
その第一段階として、5月はReactの基礎を固めていきます。教材はUdemyのこちらのコースを使用しています。
https://www.udemy.com/course/modern_javascipt_react_beginner/www.udemy.com
https://www.udemy.com/course/react_stepup/www.udemy.com
今回は、React Hooksの基本的な機能であるuseStateとuseEffectについてアウトプットしていきます。
React Hooksとは?
まず、「React Hooksとはなにか?」について解説します。
React HooksはReact 16.8以降に導入された機能で、クラスコンポーネントを使用せずに状態やライフサイクルメソッドなどの機能を関数コンポーネントで使用できるようにするためのものです。
従来のReactでは、状態を管理するためにクラスコンポーネントを使用する必要がありました。しかし、Hooksを使用することで、関数コンポーネントでも状態を管理できるようになりました。
これらのHooksを使うことで、クラスコンポーネントに依存しないよりシンプルなコードを書くことができます。初めてReactを触る人でも、Hooksを使うことでより簡単にReactアプリケーションを開発することができます。
実際に、React Hooksの登場によって、複雑なクラスコンポーネントを設計する必要がなくなり、Reactが爆発的に普及するきっかけとなったようです。
useStateとuseEffect
公式ドキュメントを参照すると、React18.2の時点でReactが公式に提供しているHooksは15個になります。今回紹介するuseState
とuseEffect
は、その中でもReactの機能の根幹をなすHooksです。
両者の役割を箇条書きすると、
となります。これだけ書いても訳がわからないと思うので、さっそく詳しい解説にいきましょう。
useState
useState
はReact Hooksの中でも最も基本的なHooksで、値の状態管理を行うために使います。
useState
の文法は以下の通りです。
const [状態, 更新関数] = useState(初期状態);
useState
関数の引数には、初期状態として設定したい値を渡します。useState
は配列を返し、配列の最初の要素には現在の状態の値、二番目の要素には状態を変更するための関数が格納されます。返り値をそれぞれ得るためには、上記のように分割代入を使用します。
以下は、カウントアップボタンをuseState
を使って実装したコンポーネントの例です。
import React, { useState } from 'react'; export const Counter = () => { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); } return ( <div> <p>Count: {count}</p> <button onClick={handleClick}>Increment</button> </div> ); }
この例では、count
という変数を宣言し、その初期値としてuseState
に0
を渡しています。次に、handleClick
関数内でsetCount
を呼び出して、count
に1
を加算した結果を新しい値として設定しています。これが、最新のcount
の値としてコンポーネント内で上書きされます。
最後に、JSXで現在のcount
の値を表示するための要素を作成し、<button>
要素をクリックした際にhandleClick
関数を呼び出すように設定しています。
useState
は、数値、文字列、オブジェクト、配列などの任意の値を扱うことができます。また、useState
で定義した値は、状態を更新するたびにコンポーネントが再レンダリングされるという特徴があります。
useEffect
コンポーネントの再レンダリング
まず、useEffect
の概念を正しく理解するために、Reactにおいてコンポーネントが再レンダリングされる条件を理解しておく必要があります。その条件とは、次の3つです。
- コンポーネント内の状態管理されている値(
state
)が変更されたとき - 親コンポーネントからプロパティ(
props
)を受け取っている場合、そのプロパティ値が変更されたとき - 親コンポーネントが再レンダリングされたとき
(例外はありますが、最初はとりあえずこの3つを覚えておけば大丈夫です。)
この中で、useEffect
の機能にかかわってくるのは、1. コンポーネント内の状態管理されている値が変更されたとき
です。どういう意味か、サンプルコードを交えながら解説します。
レンダリングの無限ループ
先ほどのuseState
の解説で示したコードに、カウントが3の倍数の時だけ「3の倍数です」という文章を表示する、というロジックを追加します。
/* ① 読み込み開始位置 */ import React, { useState } from "react"; export const Counter = () => { const [count, setCount] = useState(0); const [isShowMessage, setIsShowMessage] = useState(false); const handleClick = () => { setCount(count + 1); }; /* ② countが3の倍数かの判定 */ if (count % 3 === 0 && count !== 0) { setIsShowMessage(true); } else { setIsShowMessage(false); } /* ③ 状態の更新終了 → ①へ戻る */ return ( <div> <p>Count: {count}</p> <button onClick={handleClick}>Increment</button> {isShowMessage && <p>3の倍数です</p>} </div> ); };
useState
を使い、メッセージを表示させるフラグisShowMessage
と、その状態更新関数であるsetIsShowMessage
を定義します。条件分岐で、カウントが3の倍数ならisShowMessage
をtrue
、それ以外ならfalse
で定義します。あとは、JSXでisShowMessage
がtrue
のときだけ、<p>3の倍数です</p>
の要素を表示するよう設定しています。
一見、この実装は正しいように見えますが、実際はエラーとなります。
何が問題なのでしょうか?
エラー文には、Too many re-renders. React limits the number of renders to prevent an infinite loop.
(訳:再レンダリングが多すぎます。Reactは無限ループを防止するために、この無数のレンダリングを制限しました。)とあります。
これは、先ほど述べた、Reactにおいてコンポーネントが再レンダリングされる条件の「1. コンポーネント内の状態管理されている値が変更されたとき」によって引き起こされるものです。
どういうことかというと、まずこのコンポーネントが最初に表示されたとき、ファイルの一番上(①)から読み込みが始まります。
そして、②で「count
が3の倍数であるかどうか」の判定を行います。②の判定に応じて、isShowMessage
の値をtrue
、もしくはfalse
に更新するのですが、ここで先ほどの条件、「コンポーネント内の状態管理されている値が変更されたとき」に当てはまります。そうすると、再度コンポーネントのレンダリングが始まり、ファイルの一番上(①)に処理が戻ります。
このように、レンダリングの無限ループが発生しているため、この実装はエラーとなります。
useEffectで解決
ここまでで、コンポーネントの無限レンダリングが発生する原因は理解してもらえたと思います。では、どうすればこの無限レンダリングを解決できるでしょうか?
「すべての状態更新が終わった後でisShowMessage
の値を決定し、その後はレンダリングを行わない」という処理ができれば解決できそうです。その機能を提供するのが、まさにuseEffect
です。
useEffect
の使い方は以下の通りです。
useEffect(() => { // 副作用(レンダリング後)の処理 }, [依存配列])
useEffect
関数の第一引数には、副作用の処理を渡します。「副作用」という難しい言葉が出てきましたが、「コンポーネントのレンダリングが終了した後に行いたい処理」と解釈すればOKです。
第二引数には、依存する値を配列形式で列挙します。今回は、変数count
の値が変化したときだけフラグの判定を行いたいので、配列の中身はcount
のみです。なお、依存配列を空([]
)で定義した場合は、初期レンダリング時のみ処理が実行されます。
実際に、useEffect
をサンプルコード上で使用したものが以下のコンポーネントです。
import React, { useEffect, useState } from "react"; export const Counter = () => { const [count, setCount] = useState(0); const [isShowMessage, setIsShowMessage] = useState(false); const handleClick = () => { setCount(count + 1); }; useEffect(() => { if (count % 3 === 0 && count !== 0) { setIsShowMessage(true); } else { setIsShowMessage(false); } }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={handleClick}>Increment</button> {isShowMessage && <p>3の倍数です</p>} </div> ); };
これで、レンダリングの無限ループを起こすことなく、3の倍数のときだけメッセージを表示させることができました。
追記(2023/05/09)
ブログ読んでくれた方から指摘をいただいたので追記します。
今回の例では、isShowMessage
を状態管理可能な変数として定義し、useEffect
内で値を決定しました。しかし、実際は次のようにuseEffect
を使わずとも、直接isShowMessage
を定義するだけで処理できます。
import React, { useState } from "react"; export const Counter = () => { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); }; // 状態管理可能な値としてではなく、通常の変数として定義する const isShowMessage = count % 3 === 0 && count !== 0 ? true : false; return ( <div> <p>Count: {count}</p> <button onClick={handleClick}>Increment</button> {isShowMessage && <p>3の倍数です</p>} </div> ); };
なぜなら、count
の値が変更されると再レンダリングが走り、コンポーネント内の1行目から再び処理が走るので、毎回isShowMessage
の値を決定するロジックを通ることになるからです。
筆者は実務ではVue.jsを使用していますが、Vueではこのような場合、computed
という関数を通してisShowMessage
の値を状態管理しなければなりません。Reactの場合も、同じように状態管理しなければならないと思っていたことによる勘違いでした。
まとめ
Reactの基本的なHooksであるuseState
とuseEffect
、及びReactで再レンダリングが起こる条件について解説しました。
Vue.jsを使っていて、これまで再レンダリングのタイミングについてあまり意識することはありませんでした。なぜかと思い調べてみると、Vueではフレームワーク側がコンポーネントの依存関係を自動的に追跡し、状態が変化したときに、どのコンポーネントを再描画する必要があるかを正確に認識しているようです。
ReactのuseEffect
の処理を、Vueでは裏側で良しなにやってくれていたということですね。
追記のところでも書きましたが、このレンダリングの挙動の違いが、VueとReactの差になっているのかなと思いました。Vueの感覚でReactを書くと、私のように無駄に状態管理する値を増やしてしまいがちなので気を付けましょう(笑)