開発チームがお届けするブログリレーです!
既に公開されている記事もありますので、こちらから他のメンバーの投稿もぜひチェックしてみてください!
はじめに
本記事の対象者は、
- React初心者でとりあえずReact Hooksの概要を知りたい
- 各Hooksの説明を文字で読んでもよくわからないのでイメージ図でざっくりと理解したい
といった方向けです!
できるかぎり、初心者の方でもイメージしやすいような説明を意識しました。
そのため、細かい部分の説明は省略しております。
詳細な説明は、公式サイトを見ていただく方が良いと思いますので、
本記事でざっくりとイメージする → 公式サイトで掘り下げて理解を深める
という使い方をしていただければと思います!
React Hooksとは?
React Hooksは、React 16.8で追加された機能であり、stateなどのReactの機能をクラスを書かずにシンプルに実装することができます。
Reactでの開発をする上で避けては通れないものです!
主なHooksは以下の通りです。
- useState
- useRef
- useContext
- useEffect
- useMemo
- useCallback
こちらの6つのHooksについて解説していきます!
useState
概要
ユーザーが入力した情報などを保存することができます。
おそらく一番使用する頻度が高いHooksだと思います。
以下のように定義します。
※state , setStateは任意の名前です([○○, set○○] のように命名するのが慣習です)
以下のように、更新関数に値を設定することで状態を更新します。
使用例
以下は、ボタンを押すと数字をカウントアップするだけのシンプルな実装です。
コード:
import { useState } from "react"; export const Counter = () => { const [count, setCount] = useState(0); // useStateを定義(初期値:0) /** 現在の値+1を更新関数に設定 */ const handleClick = () => { setCount(count + 1); }; return ( <> {/* 現在の値を表示 */} <div>{count}</div> <div> {/* カウントアップするボタン */} <button onClick={handleClick}>カウントアップ</button> </div> </> ); };
画面イメージ:
useRef
概要
useStateと同様、情報を保存できます。
stateとの大きな違いは、値が変更されてもレンダリングされないことです。
stateよりもさらにシンプルで、currentプロパティに値を保持します。
以下のように定義します。
useStateと合わせたイメージだと以下のようになります。
ref.currentに、値を直接設定して更新します。
これだと、stateより単純でいいのでは?と思いがちですが、
概要でも書いたようにuseRefの値は更新されてもレンダリングされないので、
値の変化を画面に反映するような場合はstateを使う必要があります。
基本的に、useRefはあまり頻繁には使われないと公式にも記載があります。(※参考:こちら)
よく使われる例としては、以下のようなDOM操作があります。
- テキストボックスにフォーカスを当てる
- 画面をスクロールする
使用例
以下のようにすると、ボタンクリックでテキストボックスにフォーカスが当たります。
コード:
import { useRef } from "react"; export const Focus = () => { const inputRef = useRef(0); // useRefを定義(初期値:0) /** テキストボックスにフォーカスする */ const handleClick = () => { inputRef.current.focus(); }; return ( <> <div> {/* テキストボックス(refにuseRefの戻り値を渡す) */} <input ref={inputRef}></input> </div> <div> {/* テキストボックスにフォーカスするボタン */} <button onClick={handleClick}>フォーカスする</button> </div> </> ); };
画面イメージ:
useContext
概要
コンテクストの読み取りとサブスクライブを行います。
コンテクストの値を使用することで、propsのバケツリレーをしなくて済むようになります。
useContextは文字だけの説明だと分かりづらいので、上記はさらっと流して以下のイメージ図を見てみてください。
propsを使用して親コンポーネントから配下のコンポーネントに値を渡すには、
順番にバケツリレーをして渡していく必要があります。(上図の青色の矢印)
→ 中間階層で必要ない値でも、子に渡すためにpropsの記述が必要になります。
useContextを使えば、上から順番に渡す必要はなく、
コンポーネントから直接コンテクストの値を取得することができます。(上図のオレンジ色の矢印)
→ 値が必要なコンポーネントのみで記述すればOK!
使用例
まずは、コンテクストを作成します。
図で言うと、以下の赤色の破線で囲った部分の作成です。
コードは以下のようになります。
const context = createContext(null);
先ほどの図では省きましたが、useContextで値を取得するにはそのコンポーネントがコンテクストプロバイダーでラップされている必要があります。
(ラップされている範囲外からは値を取得できません)
イメージ図に追加すると以下のようになります。
そのため、以下のように、先ほど作成したプロバイダーでコンポーネントをラップします。
<context.Provider value={value}> // ここに対象のコンポーネントを置く </context.Provider>
呼び出したいコンポーネントで、useContextを使って値を取得します。
const value = useContext(context);
全体のコードは以下のようになります。
こちらの例では、3階層のコンポーネント(親・子・孫)を用意し、親で入力した値を孫コンポーネントで表示するようにしています。
import { createContext, useState, useContext } from "react"; const context = createContext(null); // コンテクスト作成 // 親コンポーネント export const ParentComponent = () => { const [value, setValue] = useState(null); // 入力した値をvalueにセットする const handleChange = (e) => { setValue(e.target.value); }; return ( <> <context.Provider value={value}> <div className="card"> <strong>親</strong> <div> <input value={value} onChange={handleChange} /> </div> <ChildComponent /> </div> </context.Provider> </> ); }; // 子コンポーネント const ChildComponent = () => { return ( <div className="card"> <strong>子</strong> <div> <GrandChildComponent /> </div> </div> ); }; // 孫コンポーネント const GrandChildComponent = () => { const value = useContext(context); return ( <div className="card"> <strong>孫</strong> <div>値:{value}</div> </div> ); };
画面イメージ:
useEffect
概要
初回のレンダリング後、または依存配列の値が変更されたときに、設定された処理を実行します。
以下のように定義します。
ここで、第2引数の依存配列について補足します。
依存配列については、以下3パターンの設定方法があります。
①配列(空でない)を渡す
useEffect(() = { (処理) }, [a,b]);
→初回レンダリング後と、依存配列の値が変更された時に実行されます
②空の配列を渡す
useEffect(() => { (処理) }, []);
→初回レンダリング後のみに実行されます
③配列を渡さない
useEffect(() => { (処理) });
→初回レンダリング、その後のレンダリングのたびに実行されます
③については、useEffect内の処理に関係のないstateが更新された場合でも実行されるので、
通常は①か②を設定することになります。
※詳細はこちらを参照
使用例
コード:
import { useEffect, useState } from "react"; export const Effect = () => { const [count, setCount] = useState(0); // useStateを定義(初期値:0) // 現在の値+1を更新関数に設定 const handleClick = () => { setCount(count + 1); }; useEffect(() => { console.log("カウント回数:", count); }, [count]); useEffect(() => { console.log("初回のみ表示"); }, []); useEffect(() => { console.log("レンダリングのたびに表示"); }); return ( <div> {/* カウントアップするボタン */} <button onClick={handleClick}>カウントアップ</button> </div> ); };
画面イメージ:
4回ボタンを押すと、コンソールログに上記のように表示されます。
「初回のみ表示」の処理が初めの一回だけ実行されること、
ボタンを押すたびにcountが変化することで「カウント回数」・「レンダリングのたびに表示」の処理が実行されていることがわかります。
注意:
useEffectを使用する際、以下のようにしてしまうと無限ループが発生してしまいますので注意してください。
const [count, setCount] = useState(0); // useStateを定義(初期値:0) useEffect(() => { setCount(count + 1); // ここでcountが更新されるため、それをトリガに再度関数が動く→無限ループになる }, [count]);
useMemo
概要
不要なレンダリングをスキップするため、計算結果をキャッシュすることができます。
以下のように定義します。
上記のように設定した場合、動作は以下のようになります。
- 初回のレンダリング:calculateValueを呼び出し、結果を返します
- 2回目以降のレンダリング:
- dependenciesが前回から変更していない場合:関数は呼び出さず、キャッシュした値を返します
- dependenciesが前回から変更している場合:関数を再度呼び出し、結果を返します
使用例
コード:
※以下では、メモ化した(useMemoを使用した)パターンと、メモ化していない(useMemoを使用しない)パターンのカウントアップのボタンを用意しています。
import { useState, useMemo } from "react"; export const Memo = () => { const [count, setCount] = useState(0); // useStateを定義(初期値:0) const [count2, setCount2] = useState(0); // useStateを定義(初期値:0) // 現在の値+1を更新関数に設定 const handleClick = () => { setCount(count + 1); }; const handleClick2 = () => { setCount2(count2 + 1); }; // 引数を2乗したものを返す const squaredNumber = (number) => { console.log("2乗の計算を実行"); return number * number; }; // メモ化する const squaredNumberMemo = useMemo(() => squaredNumber(count), [count]); // メモ化しない const squaredNumberNotMemo = squaredNumber(count2); return ( <> {/* メモ化した場合 */} <div className="card"> <div>カウント回数:{count}</div> <div>2乗の値:{squaredNumberMemo}</div> <div> <button onClick={handleClick}>カウントアップ1</button> </div> </div> {/* メモ化しない場合 */} <div className="card"> <div>カウント回数2:{count2}</div> <div>2乗の値:{squaredNumberNotMemo}</div> <button onClick={handleClick2}>カウントアップ2</button> </div> </> ); };
画面イメージ:
カウントアップ1をクリックした時:
カウントアップ2をクリックした時:
カウントアップ1の関数のみメモ化しているため、
カウントアップ1をクリックすると1と2どちらの関数も動きますが、
(=コンソールにメッセージが2回ずつ表示されている)
カウントアップ2をクリックした場合、カウントアップ1の関数は動きません。
(=コンソールにメッセージが1回ずつしか表示されない)
→画面に表示される結果は同じですが、不要なレンダリング(カウントアップ2をクリックした時のカウントアップ1の計算)をなくすことができています。
useCallback
概要
不要なレンダリングをスキップするため、関数定義をキャッシュすることができます。
useMemoは計算結果をキャッシュしますが、こちらは関数定義自体をキャッシュします。
以下のように定義します。
上記のように設定した場合、動作は以下のようになります。
- 初回のレンダリング:fn関数を返します(実行はしません)
- 2回目以降のレンダリング:
- dependenciesが前回から変更していない場合:キャッシュしたfn関数を返します
- dependenciesが前回から変更している場合:今回のレンダリング時に渡されたfn関数を返します
使用例
useCallbackを理解するには、useMemoとの関係を考えると分かりやすいので、
useMemoの例と合わせて使用例を記載します。
以下は公式チュートリアルの内容を引用しています:
関数定義のメモ化をuseMemoで書くと以下のようになります。
export default function Page({ productId, referrer }) { const handleSubmit = useMemo(() => { return (orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails }); }; }, [productId, referrer]); return <Form onSubmit={handleSubmit} />; }
useMemoの第一引数には、「キャッシュしたい値を計算する関数」を設定するので、
上記のように「関数定義をreturnする関数」を設定する必要があります。
入れ子になっていて分かりづらいですね・・・
それを解決するためのuseCallbackです。以下のようになります。
export default function Page({ productId, referrer }) { const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails }); }, [productId, referrer]); return <Form onSubmit={handleSubmit} />; }
※詳細はこちらを参照。
入れ子構造がなくなり、すっきりしました。
useMemoがわかっていれば、useCallbackも理解しやすいかなと思います。
まとめ
主要なReact Hooksの概要をまとめてみましたが、ざっくりとでもイメージできたでしょうか?
このフックでこんなことがしたい、など想像を膨らませていただければと思います!
Reactでの開発を楽しんでもらうための一助になれば幸いです。