こんにちは。
エンジニアのNkawaKです。
最近ではAmazon Connectを使った弊社サービスのSylphinaや音声認識エンジンの認者など、
Reactを使ってサービスのUIを開発しています。
本記事ではReactのメイン機能であるReact Hooksの解説をしていきたいと思います。
サンプルのソースコードも掲載しているので参考にしていただけましたら幸いです。
目次
React Hooksとは
React Hooksはv16.8から追加された新機能で、Hooksの追加以前と比べてReactは大きく変わりました。
Reactではクラスコンポーネントと関数コンポーネントの2種類があり、v16.8以前ではクラスコンポーネントが主流でした。
この理由は関数コンポーネントでは状態(state)を持つことができないからでした。
そしてv16.8からHooksの追加により、関数コンポーネントに状態を持たせることが可能になりました。
Hooksによって関数コンポーネントの状態によってUIを切り替えたり、外部APIからデータを取得することが可能になるなど、
Hooksの恩恵は非常に大きく最近のReactではHooksを使った関数コンポーネントが主流になっています。
Hooksを習得することでReactの最新トレンドにキャッチアップすることができるでしょう。
それではReact Hooksを使ったコードを見ていきましょう。
基本のHooks
まずはReactを使う上で基本になるHooksを解説していきます。
基本のHooksはuseState、useEffect、useContextの3つがあります。
とくにuseStateとuseEffectは使用頻度が高く、Reactを使う上で必須の機能です。
useState
コンポーネントに状態を持たせて、状態の変更に合わせて再レンダリングを行うHookです。
基本中の基本でReactで最もお世話になるHookになります。
1 |
const [count, setCount] = useState(0); |
useStateではタプルでstateの値とセッターを定義します。
useState(0)のように引数の値がstateの初期値になります。
カウンターのボタンを押すとstateのcountが変化し、それに合わせてカウントの表示が変わります。
ボタンのクリックイベントでセッターを使用して、countを1ずつ足し引きします。
1 |
onClick={() => setCount((c) => c + 1)} |
setCountのコールバックの第一引数には現在のstateの値が渡されます。
ちなみにですが、セッターは以下のようにも書くことができます。
1 |
onClick={() => setCount(count + 1)} |
この書き方だと常にstateの最新の値を参照するとは限らないため、
コールバックの引数を使用するのが望ましいです。
今回のように数値に値を加えるような場合でなければ、
setSometing(“hoge”)のように引数に値をそのまま設定しても問題ありません。
useEffect
useEffectはコンポーネントに副作用を発生させるHookです。
副作用とはコンポーネントに変化をもたらすあらゆる変化のことです。
このサンプルではCountの数が10を超えるか、0を下回るとアラートを表示する副作用を発生させています。
1 2 3 4 5 6 7 |
useEffect(() => { if (count > 10) { alert(`${count} is over 10.`); } else if (count < 0) { alert(`${count} is under 0.`); } }, [count]); |
useEffectは第一引数に副作用として発生させる処理を含む関数を記述し、
第二引数に副作用のトリガーになる変数を含んだ配列を追加します。
第二引数の変数に差分が発生した場合は、副作用の関数を実行します。
第二引数の配列を空([])の状態で渡すことも可能です。
その場合は初回のレンダリング時にだけ関数が実行されます。
例えば、以下のサンプルでは最初のレンダリングでユーザー一覧を取得しています。
第二引数を省略することもできます。
省略した場合は再レンダリングの度に関数が実行されます。
そうするとstateが変更される度に副作用が起こるので、パフォーマンスに影響が出たり、
制御が難しくなるため注意する必要があります。
またuseEffectはコンポーネントが画面から外れる際の処理を追加する場合にも使用します。
例えば、以下のようにWebSocketの接続を閉じる処理が挙げられます。
1 2 3 4 5 6 7 8 9 10 11 |
let webSocketObj; const connectWebSocket = () => { webSocketObj = new WebSocket("ws://0.0.0.0:0000"); // do somethings. }; useEffect(() => { connectWebSocket(); return () => webSocketObj.close(); }, []); |
第一引数の関数から、さらに関数をreturnして処理を追加します。
useEffectを使う場合に無限ループの発生に注意する必要があります。
例えば以下のように第二引数に設定しているstateをuseEffectの中で変更すると無限ループが起こります。
1 2 3 4 |
useEffect(() => { console.log(count); setCount((c) => c + 1); }, [count]); |
useEffectの処理が長くなるとやってしまいがちなミスです。
useEffectが依存している変数の扱いには注意しましょう。
useContext
useContextはpropsを介することなく、全ての下位コンポーネントにデータを渡すことができるHookです。
上のサンプルではフォームに文字が入力されてボタンが押されると、下位のコンポーネントで入力された文字を表示します。
Reactでは通常では上から下へと、配下のコンポーネントにはpropsを使ってデータを渡していきます。
1 2 3 4 5 6 7 |
const Parent = (props) => { return ( <Child data={props.data} /> ); }; |
上のように親と子の2階層だけならpropsで渡すのも問題ないのですが、これに加えて孫から曽孫と階層が深くなるにつれて
propsでデータを渡すのはバケツリレーのような大変煩雑な作業になる上に、どこに元のデータがあるのか探すのも難しくなります。
こうした手間を解消してくれるのがuseContextです。
useContextを使用するには、最初にcreateContextでコンテクストオブジェクトを作成します。
1 |
const userNameContext = React.createContext(""); |
createContextの引数に渡した値が初期値になります。
次にデータを渡したいコンポーネントをContext.Providerで囲みます。
1 2 3 |
<userNameContext.Provider value={userName}> <MyPage/> </userNameContext.Provider> |
この時にコンテクストオブジェクトのvalueを設定します。
これでContext.Providerの配下にある全てのコンポーネントでデータを受け取れるようになります。
コンテキストのデータを取り出す時は、useContextの引数に作成したコンテクストオブジェクトを設定します。
1 |
const userName = useContext(userNameContext); |
今回の例では入力した文字列を受け取るだけですが、他にもオブジェクトや関数などもコンテクストに設定することができます。
追加のHooks
前章ではReactを使ってUIを作成するうえで、基本になるHooksを解説しました。
大体のユースケースでは基本のHooksで事足りるのですが、パフォーマンスの向上やstateの管理を厳密にするなどの目的で追加のHooksを使用します。
この章ではuseRef、useMemo、useCallback、useReducerを解説していきます。
useRef
useRefは.currentプロパティに値を保持することができるrefオブジェクトを生成するHookです。
よくある使われ方はDOMへの参照を保存することです。
今回の例ではinputタグのDOMをrefオブジェクトに保存して、ボタンに設定した関数の中でフォーカスを行なっています。
1 2 3 |
const handleClick = () => { inputRef.current.focus(); }; |
DOMへの参照を保存するには、対象のタグの中でrefというパラメーターに作成したrefオブジェクトを設定します。
1 |
<input ref={inputRef}/> |
またrefオブジェクトに保存した値は書き換え可能で、useStateと違って変更しても再レンダリングが発生しないという特徴があります。
このことから画面の表示には使わないが、内部的によく利用するデータを保持しておくという用途でも使用します。
useMemo/useCallback
useMemoとuseCallbackはメモ化と呼ばれる、プログラムを高速化する手法をHookで行うために使用します。
処理が重い関数を何度も実行するのはリソースの無駄なので、その関数の計算結果をメモとして保存しておき
必要な時だけ再計算が行われるようにしておくことでパフォーマンスの最適化を図ります。
上記のサンプルではフォームに秒数を入力して、スタートボタンを押すとカウントが開始されて
残り時間が3分の1になると文字が赤くなり、0になると再度カウントが開始されます。
リセットボタンを押すとカウントが最初に戻ります。
サンプルの中ではカウント秒数の3分の1にあたる数を計算する部分と、カウントのリセットを行う関数をメモ化しています。
まずはuseMemoを使っている部分を見ていきましょう。
1 2 3 4 5 6 7 8 9 10 |
const getThirdPartNumbers = (number) => { if (!number && number < 0) return []; return [...Array(number + 1).keys()].slice(1).slice(0, number / 3); }; const thirdPartNumbers = useMemo(() => getThirdPartNumbers(startTimeRef.current), [startTimeRef.current]); <p className={thirdPartNumbers.includes(time) ? "red" : undefined}> {time} </p> |
getThirdPartNumbersでは引数の3分の1にあたる数を配列で返します。
この関数をコンポーネントの中で使用して、カウントに入力された数(startTimeRef.current)を引数にして計算結果を受け取ります。
カウントが進んでいる数がこの配列の中に含まれていた場合は、クラスを付け替えて文字を赤く強調します。
この計算結果を受け取る部分でuseMemoを使用してメモ化を行います。
メモ化を行わない場合はカウントが進むたびに再レンダリングが発生するため、getThirdPartNumbersがレンダリングのたびに再実行されてしまいます。
今回の例は重い計算ではないのでパフォーマンスには大した影響は出せんが、もっと重い処理だと見過ごせない影響が出るはずです。
useMemoを使えばgetThirdPartNumbersの計算を入力されたカウント数が変わった時だけ行うことができます。
useMemoは第一引数に計算を行う関数を指定し、第二引数では再計算を行うトリガーにする値を含めた配列を指定します。
インターフェイスはuseEffectと同じです。今回の例ではstartTimeRef.currentが変わった時だけ計算を実行して、不要な再計算を防いでいます。
次にuseCallbackを見ていきましょう。
useCallbackは関数そのものをメモ化するHookです。
この例ではタイマーのリセットを行う関数resetにuseCallbackを使用しています。
スタートボタンを押した時に、タイマーのカウントを開始するuseEffectの部分で以下のようなメッセージが表示されていました。
useEffectの中で使用している関数resetが依存配列の中にないという警告です。
しかし、useCallbackを使用することなくresetを依存配列に追加するとさらに警告が表示されます。
関数resetはレンダリング毎にuseEffectの依存関係が変わってしまうため、これをuseCallbackでラップするよう言われています。
関数コンポーネントではレンダリング毎に関数が再生成されて参照先が変わるため、
そのままだと毎秒resetが実行されてカウントが進まなくなってしまいます。
この問題を修正するためにresetをuseCallbackでラップし、関数の定義をメモ化します。
1 2 3 4 5 6 |
const reset = useCallback(() => { if (!isStart) return; clearTimer(); timerIdRef.current = setInterval(() => setTime((t) => t - 1), 1000); setTime(startTimeRef.current); }, [startTimeRef.current]); |
秒数が変更された場合に、resetを再生成するために第二引数の配列にstartTimeRef.currentを追加しています。
これでカウントが進まなくなる問題が修正され、
またカウント中に秒数が変更された場合には、resetを再実行してカウントを戻すことができるようになりました。
以上がuseMamoとuseCallbackの解説になります。
useMemoは計算結果をメモ化し、useCallbackは関数の定義自体をメモ化すると覚えておきましょう。
useReducer
最後にuseReducerを解説します。
useReducerはstate管理のロジックを1箇所にまとめることができるHookです。
useReducerはuseStateと同じくstateを管理しますが、reducerと呼ばれる関数によってstateを更新することが特徴です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const reducer = (state, action) => { switch(action.type) { case "ADD": return [...state, { id: action.payload.id, title: action.payload.title }]; case "UPDATE": return state.map(item => item.id === action.payload.id ? { ...item, title: action.payload.title } : item ); case "DELETE": return state.filter(item => item.id !== action.payload.id); default: throw new Error(); } }; |
このサンプルでは追加、更新、削除の処理をreducerにまとめています。
useReducerを使用する際はこのreducerを第一引数に指定し、第二引数にはstateの初期値を設定します。
1 |
const [todos, dispatch] = useReducer(reducer, initialState); |
タプルの最初の要素がstateの値となり、2つ目にはdispatchというreducerにstateの更新を通知する関数が設定されます。
stateを更新する際には、以下のようにdispatchを実行します。
1 2 3 4 5 6 |
dispatch({ type: "ADD", payload: { id: todos.length ? todos.slice(-1)[0].id + 1 : 1, title: title } }); |
dispatchの引数のオブジェクトには、reducerの条件に合致する文字列(type)と更新する値(payload)を設定しています。
dispatchの引数に渡したオブジェクトが、reducerの第二引数のactionに含まれています。
以上のようにして、useReducerは少し手間を加えてstateを更新するのですが、それに見合ったメリットがあります。
まず第一にstateを更新するロジックがreducerにまとめられている点が挙げられます。
useReducerを使わない場合は追加、更新、削除を行う関数を3つ作る必要がありますが、その時点で3つの関数を管理しなければならなくなります。
今回のサンプルではコンポーネントは一つだけですが、下位のコンポーネントでもこれらの関数を使う場合には、
どのコンポーネントに関数が定義されているのか探す手間も出てくると思います。
その点useReducerを使えば、stateの更新を行うロジックがreducerにまとめられているので管理する関数は一つで済みますし、
どのようにstateを更新するのかもreducerから一目で分かります。
またreducerがインターフェイスとして機能する点も挙げられるでしょう。
useReducerで定義されたstateはreducerに合致する条件でしか更新ができないので、下位のコンポーネントで勝手な変更ができなくなります。
これは他のメンバーと開発をする上で、バグを減らすことに繋がります。
さらに下位のコンポーネントに渡ってstateの更新を行う場合には、useReducerを使う方がパフォーマンスの最適化にも貢献します。
以上のようにuseReducerは複雑な状態管理を簡単にしてくれるHookになります。
まとめ
- useStateはコンポーネントにstateを定義し、stateを変更して再レンダリングを行う
- useEffectはコンポーネントに発生させる副作用を定義する
- useContextは下位コンポーネントでデータを受け取るために使用する
- useRefは.currentにDOMへの参照など様々なデータを保存することができる
- useMemoは関数の計算結果をメモ化する
- useCallbackは関数の定義自体をメモ化する
- useReducerは複雑な状態管理をreducerにまとめることができる
React Hooksに関する解説は以上になります。
ここまでご覧いただきありがとうございました。React学習の参考になりましたら幸いです。
参考文献
https://ja.reactjs.org/
りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅱ. React基礎編】
りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅲ. React応用編】
- 【React】フロントエンドのテストコードを書いてみよう【Vitest】 - 2024-04-30
- React Hooks入門ガイド - 2022-03-07
- Vue.jsで構成するシングルページアプリケーション(SPA)の作り方やサンプル例【後編】 - 2020-02-07
- Vue.jsで構成するシングルページアプリケーション(SPA)の作り方やサンプル例【前編】 - 2019-08-05