本記事は 電通国際情報サービス Advent Calendar 2021 の 13 日目の記事です。 執筆者は 2021 年新卒入社の XI 本部 AI トランスフォーメンションセンター所属の山田です。 はじめに React Hooks とは useState useState を使うユースケース useEffect useEffect を使うユースケース useContext useContext を使うユースケース useReducer useReducer を使うユースケース useMemo useMemo を使うユースケース useCallback React.memo と useCallback useCallback を使うユースケース React Hooks を正しく使うために おわりに はじめに 本記事では React Hooks の代表的なフックについて、その使い方と ユースケース をサンプルコードとともに紹介します。 サンプルコードは TypeScript で記述しています。 React Hooks とは React Hooks は React 16.8(2019 年 2 月リリース)で追加された機能です。 2021 年現在において React でアプリケーションを構築するためには理解が必須の機能といっても過言ではないでしょう。 React Hooks を使うことによって React の関数型 コンポーネント で状態(state)を持つことや コンポーネント のライフサイクルに応じた処理を記述できます。 以下では、 React で提供される基本的な React Hooks をその ユースケース とともに紹介します。 useState useEffect useContext useReducer useMemo useCallback useState useState は関数型 コンポーネント で状態(state)を扱うためのフックです。 以下は useState を利用する場合の基本的なコードです。 // 返り値はstateの変数とstateを更新するための関数 const [ state , setState ] = useState < T >( initStateValue ); useState は状態(state)の変数と状態(state)を更新するための関数を返します。 状態(state)の更新をする際は必ず更新用の関数を介して行う必要があります。 useState を使う ユースケース useState が必要となるのは、利用者と インタラクティブ にやり取りをする値を保持する必要がある場合です。 利用者と インタラクティブ にやり取りをするという場面の最も典型的な例はフォームです。 ここではログインフォームを題材にしてコードを紹介します。 作成するログインフォームは画像のように input 要素としてユーザー ID とパスワードを持つものを想定します。 初めにログインフォームで扱うデータの型(SampleLoginForm)を定義しておきます。 今回の例では userId と password だけをプロパティに持つオブジェクトとします。 interface SampleLoginForm { userId: string ; password: string ; } 作った SampleLoginForm 型の変数 formData を useState を使って定義します。 const [ formData , setFormData ] = useState < SampleLoginForm >( { userId: "" , password: "" , } ); あとは input 要素の value 属性に対応する formData の変数を渡します。 さらに input 要素の onChange イベントから setFormData を呼び出して formData の状態を更新します。 < div > < label htmlFor = "userId" > ユーザーID < /label > < input id = "userId" type= "text" name = "userId" placeholder = "ユーザーID" value = { formData.userId } onChange = { ( e ) => setFormData ( { ...formData , userId: e.target.value } ) } / > < /div > これにより、ユーザーがフォームに入力した文字(値)を変数 formData に保持できます。 ▶︎ クリックしてコード全文を見る // components/LoginForm.tsx import React , { useState } from "react" ; interface SampleLoginForm { userId: string ; password: string ; } export default function LoginForm () : JSX. Element { const [ formData , setFormData ] = useState < SampleLoginForm >( { userId: "" , password: "" , } ); const submitHandler = ( e: FormEvent < HTMLFormElement >) => { e.preventDefault (); console .log ( "ログインボタン押下" , formData ); } ; return ( < form onSubmit = { submitHandler } > < div > < label htmlFor = "userId" > ユーザーID < /label > < input id = "userId" type= "text" name = "userId" placeholder = "ユーザーID" value = { formData.userId } onChange = { ( e ) => setFormData ( { ...formData , userId: e.target.value } ) } / > < /div > < div > < label htmlFor = "password" > パスワード < /label > < input id = "password" type= "password" name = "password" placeholder = "パスワード" value = { formData.password } onChange = { ( e ) => setFormData ( { ...formData , password: e.target.value } ) } / > < /div > < div > < button type= "submit" > ログイン < /button > < /div > < /form > ); } useEffect useEffect は関数型 コンポーネント で副作用を実行するためのフックです。 副作用と聞くと仰々しいですが コンポーネント 内での「外部データの取得」「DOM の手動での更新」などの処理を、React では副作用と呼びます。 useEffect を使うための基本的なコードは以下のとおりです。 // 副作用を含む処理を記述した関数を記述する useEffect (() => { // 副作用処理 // … return () => { // クリーンアップ処理 } ; } , [] ); useEffect では副作用となる処理を関数内で記述します。 return で関数を返すことによってクリーンアップ処理を記述できます。 通常、 useEffect による副作用処理は コンポーネント の レンダリング 毎に実行されます。 副作用処理を毎回行わないためには、第2引数の依存配列によって制御できます。 useEffect の詳しい説明については以下の参考リンクをご覧ください。 副作用フックの利用法 https://ja.reactjs.org/docs/hooks-effect.html useEffect完全ガイド https://overreacted.io/ja/a-complete-guide-to-useeffect/ useEffect を使う ユースケース useEffect が必要となる代表的な ユースケース としては コンポーネント を呼び出したタイミングで外部 API からリソースを取得したい場合などです。 ここではサンプルの外部 API として JSONPlaceholder を利用して コンポーネント を呼び出したタイミングでデータを取得してみましょう。 JSONPlaceholder, https://jsonplaceholder.typicode.com/ 少し JSONPlaceholder について補足します。 JSONPlaceholder は 6 種類の構造のダミーデータを取得できます。 今回はタスク管理アプリケーションで一般的な ToDo リスト形式のデータを取得します。 取得したデータは先ほど紹介した setState を使って保持します。 まず取得する ToDo リストの型を定義しておきます。 interface ToDo { id: number ; userId: number ; title: string ; completed: boolean ; } そして先ほどの useState フックを使って取得する ToDo リスト形式を状態管理します。 const [ todoItemss , setToDos ] = useState < ToDo [] >( [] ); そして useEffect を使って実際に外部 API を呼び出し、状態を更新します。 外部 API の呼び出しには fetch を利用します。 useEffect (() => { const f = async () => { const res: Response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ); const json: ToDo [] = await res.json (); setToDos ( json ); } ; f (); } , [] ); 注意点として useEffect に渡す関数は同期的です。 そのため非同期関数(async/await)を使うには関数内で定義する必要があります。 補足ですが、次期アップデートの React v18 より React.Suspense を使った非同期のデータ取得がサポートされます。 アップデート後はこちらがベストプ ラク ティスになっていく可能性も高いため、公式ドキュメントの「React.Suspense」と「サスペンスを使ったデータ取得」についても、ぜひチェックをしてみてください。 React の最上位 API - React.Suspense, https://ja.reactjs.org/docs/react-api.html#reactsuspense サスペンスを使ったデータ取得(実験的機能), https://ja.reactjs.org/docs/concurrent-mode-suspense.html 取得した ToDo リスト形式のデータはスタイルを少し当てて Array.prototype.map() を使えば以下のように描画できます。 ▶︎ クリックしてコード全文を見る // components/ToDoList.tsx import React , { useEffect , useState } from "react" ; interface ToDo { id: number ; userId: number ; title: string ; completed: boolean ; } export default function ToDoList () : JSX. Element { const [ todoItems , setToDos ] = useState < ToDo [] >( [] ); useEffect (() => { const f = async () => { const res: Response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ); const json: ToDo [] = await res.json (); setToDos ( json ); } ; f (); } , [] ); return ( < div style = {{ textAlign: "left" }} > { todoItems.map (( todoItem ) => ( < div key = { todoItem.id } style = {{ width: "250px" , border: "solid" , margin: "8px" , padding: "8px" , }} > < h4 > { todoItem.title } < /h4 > < p style = {{ textAlign: "right" }} > { todoItem.completed ? "✅ 完了" : "未実施" } < /p > < /div > )) } < /div > ); } useContext useContext は コンポーネント 間で横断的に利用したい状態を管理するためのフックです。 通常、 コンポーネント では状態(データ)を props を通して親から子に渡します。 これを図に起こすと以下のようになります。 一方、コンテキストを使うと以下のように props を通さずにデータをやり取りできます。 コンテキストでは Context.Provider コンポーネント を通して横断的に利用したい状態を配信します。 そして必要な コンポーネント で useContext を使うことによって状態を購読します。 useContext を使って コンポーネント 内でコンテキストから配信される値を購読する基本的なコードは以下のようになります。 // 返り値はコンテキストから配信される値 // useContextの第1引数には`React.createContext`によって作成したコンテキストオブジェクトを渡す const value = useContext ( MyContext ); useContext では購読するコンテキストのオブジェクトを渡し、コンテキストから配信される値を受け取ります。 useContext を使う ユースケース ここまでで述べてきたように useContext を使うのは コンポーネント 間で横断的に利用したい状態がある場面です。 代表的な場面として認証情報の管理などがあります。 ここではコンテキストを使ってユーザー ID を管理することを例に説明します。 管理するユーザー ID は useState を用いて宣言し、その状態と更新用の関数をコンテキストを使って配信します。 // コンテキストで配信する値 const [ userId , setUserId ] = useState < number >( -1 ); 配信する値が決まったので、コンテキストで配信する値の型を定義します。 interface Context { userId: number ; setUserId: Dispatch < SetStateAction < number >>; } createContext を使ってコンテキストオブジェクトを作成します。型引数には先ほど定義した型を指定し、第 1 引数には初期値を与えます。 const AuthContext = createContext < Context >( { userId: -1 , setUserId: () => {} , } ); 次に Context の Provider を作成します。 Provider の value プロパティにコンテキストで配信する値を指定します。 const AuthProvider: React.FC = ( { children } ) => { // コンテキストで配信する値 const [ userId , setUserId ] = useState < number >( -1 ); return ( < AuthContext.Provider value = {{ userId , setUserId }} > { children } < /AuthContext.Provider > ); } ; // コンテキストオブジェクトとProviderをexportする export { AuthContext , AuthProvider } ; createContext で作成した AuthContext と AuthProvider を外部に公開(export)することでコンテキストを利用しやすくしています。 ▶︎ クリックしてコード全文を見る // contexts/auth.tsx import React , { createContext , Dispatch , SetStateAction , useState , } from "react" ; interface Context { userId: number ; setUserId: Dispatch < SetStateAction < number >>; } const AuthContext = createContext < Context >( { userId: -1 , setUserId: () => {} , } ); const AuthProvider: React.FC = ( { children } ) => { const [ userId , setUserId ] = useState < number >( -1 ); return ( < AuthContext.Provider value = {{ userId , setUserId }} > { children } < /AuthContext.Provider > ); } ; // コンテキストオブジェクトとProviderをexportする export { AuthContext , AuthProvider } ; 作成した AuthProvider を App.tsx に記述します。 これによりアプリケーション内のどの コンポーネント でも useContext を使って AuthContext から値を購読できます。 // App.tsx import React from "react" ; import { AuthProvider } from "./contexts/auth" ; import LoginForm from "./components/LoginForm" ; import ToDoList from "./components/ToDoList" ; export default function App () : JSX. Element { return ( < AuthProvider > < div style = {{ padding: "8px" , textAlign: "center" }} > < LoginForm / > < ToDoList / > < /div > < /AuthProvider > ); } 実際に LoginForm と ToDoList コンポーネント でコンテキストを使ってみましょう。 まず LoginForm コンポーネント 内でフォーム送信時にコンテキストの userId を更新してみます。 // AuthContextからuserIdを更新する関数setUserIdを購読 const { setUserId } = useContext ( AuthContext ); // form要素のsubmitイベントを処理する関数 const submitHandler = ( e: FormEvent < HTMLFormElement >) => { e.preventDefault (); console .log ( "ログインボタン押下" , formData ); // AuthContextで配信される値userIdを更新 setUserId ( 1 ); } ; 次に ToDoList コンポーネント でコンテキストから userId を購読します。 そして useEffect で userId の状態を監視し、初期値(-1)でない場合に外部 API からリソースを取得するようにします。 // AuthContextからuserIdを購読 const { userId } = useContext ( AuthContext ); useEffect (() => { const f = async () => { const res: Response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ); const json: ToDo [] = await res.json (); setToDos ( json ); } ; // userId が初期値でない場合に外部APIコール if ( userId !== -1 ) { f (); } } , [ userId ] ); 以上でログインフォームのログインボタンを押下することで AuthContext の userId を更新し、その変更を検知して ToDo リストの情報を外部 API から取得する処理が実現できます。 useReducer useReducer は useState よりも複雑な状態を管理するためのフックです。 公式ドキュメントでは「 useState の代替品」として位置づけられています。 useReducer を使うための基本的なコードは以下のとおりです。 // `useState`の代替品。返り値はstateの変数とstateを更新するためのDispatch関数 const [ state , dispatch ] = useReducer ( reducer , initialArg , init ); useReducer を理解するためには 4 つの要素を理解する必要があります。 State … 状態 Reducer … State を更新するための関数 Action … State を更新するのに必要なデータ Dispatch … Action を Reducer に届ける関数 この 4 つの要素は図のような関係になります。 useReducer を使う ユースケース アプリケーション開発を進めていくと処理が複雑になるにつれて、管理しなければならない状態(state)が増えていきます。 また実際には、相互に関連する状態を更新しなければならない場面も増えます。 そのような場面で力を発揮するのが useReducer フックです。 例えば、先ほどの useEffect フックでを使った外部 API からのリソース取得を例に考えてみましょう。 外部リソースの取得では取得までに時間を要しますので読み込み中か否かを isLoading のような形で状態管理する必要があるでしょう。 さらにデータ取得時のエラーハンドリングを考えるとエラーが発生したかを error のような変数で状態管理する必要があります。 これらを useState フックで管理する場合は以下のようになります。 const [ todos , setToDos ] = useState < ToDo [] >( [] ); const [ isLoading , setIsLoading ] = useState < boolean >( true ); const [ error , setError ] = useState < boolean >( false ); このように複数の値に関連する状態を管理する場面で useReducer を使うことを考えます。 まず useReducer で管理する状態の型とその状態の初期値を定義します。 // 管理する状態の型 interface State { todoItems: ToDo [] ; isLoading: boolean ; error: boolean ; } // 状態の初期値 const initState: State = { todoItems: [] , isLoading: true , error: false , } ; 次に状態を更新するためのデータとなるアクションの型を定義します。 今回は状態を更新する操作として以下の 2 種類を考えます。 SET_TODOS … ToDo リストにアイテムをセットする操作。アクションは ToDo リストにセットするデータを含む。 SET_ERROR … エラーが発生した際にエ ラーフラ グを True にする操作。アクションはデータを持たない。 これらを型に起こします。 // アクションの種類 type ActionType = "SET_TODOS" | "SET_ERROR" ; // アクションの型 interface Action { type : ActionType ; payload?: ToDo [] ; } 上で定義した型を使って reducer 関数を作成します。 import { Reducer } from "react" ; const reducer: Reducer < State , Action > = ( state , action ) => { switch ( action. type) { case "SET_TODOS" : if ( ! action.payload ) { // payloadが含まれていなければエラー扱いにする return { ...state , error: true , isLoading: false , } ; } return { ...state , ...action.payload , isLoading: false , } ; case "SET_ERROR" : return { ...state , error: true , isLoading: false , } ; } } ; この reducer 関数と状態の初期値を使って useReducer を宣言します。 const [{ todoItems , error , isLoading } , dispatch ] = useReducer ( reducer , initState ); そして先程の useEffect 内で状態を更新していた部分を dispatch にアクションを渡すことで状態を更新するように書き換えます。 useEffect (() => { const f = async () => { try { const res: Response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ); const json: ToDo [] = await res.json (); dispatch ( { type : "SET_TODOS" , payload: { todoItems: json } } ); } catch ( e ) { console .log ( e ); dispatch ( { type : "SET_ERROR" } ); } } ; f (); } , [] ); この例だともともとがそこまで複雑な状態管理ではなかったため、 useReducer を使った記述が冗長だと感じるかもしれません。 どのタイミングで useReducer を使うのかは、個人/チーム次第ではありますが、うまく使うことで状態管理をわかりやすくできます。 ▶︎ クリックしてコード全文を見る // components/ToDoList.tsx import React , { Reducer , useEffect , useReducer } from "react" ; interface ToDo { id: number ; userId: number ; title: string ; completed: boolean ; } interface State { todoItems: ToDo [] ; isLoading: boolean ; error: boolean ; } const initState: State = { todoItems: [] , isLoading: true , error: false , } ; type ActionType = "SET_TODOS" | "SET_ERROR" ; interface Action { type : ActionType ; payload?: Partial < State >; } const reducer: Reducer < State , Action > = ( state , action ) => { switch ( action. type) { case "SET_TODOS" : if ( ! action.payload ) { // payloadが含まれていなければエラー扱いにする return { ...state , error: true , isLoading: false , } ; } return { ...state , ...action.payload.todoItems , isLoading: false , } ; case "SET_ERROR" : return { ...state , error: true , isLoading: false , } ; } } ; export default function ToDoList () : JSX. Element { const [{ todoItems , error , isLoading } , dispatch ] = useReducer ( reducer , initState ); useEffect (() => { const f = async () => { try { const res: Response = await fetch ( "https://jsonplaceholder.typicode.com/todos" ); const json: ToDo [] = await res.json (); dispatch ( { type : "SET_TODOS" , payload: { todoItems: json } } ); } catch ( e ) { console .log ( e ); dispatch ( { type : "SET_ERROR" } ); } } ; f (); } , [] ); return ( <> { isLoading ? ( < p > ロード中です… < /p > ) : error ? ( < p > エラーが発生しました。 < /p > ) : ( < div style = {{ textAlign: "left" }} > { todoItems.map (( todoItem ) => ( < div key = { todoItem.id } style = {{ width: "250px" , border: "solid" , margin: "8px" , padding: "8px" , }} > < h4 > { todoItem.title } < /h4 > < p style = {{ textAlign: "right" }} > { todoItem.completed ? "✅ 完了" : "未実施" } < /p > < /div > )) } < /div > ) } < / > ); } useMemo useMemo は関数の返り値をメモ化するフックです。 メモ化はプログラムの最適化技法の 1 つで、計算結果を再利用するために保持して、再計算を防ぐものです。 そのため useMemo は最適化のためのフックという位置付けです。 useMemo を使うための基本的なコードは以下のとおりです。 // 返り値は関数の計算結果をメモ化した値 // 第2引数の依存配列に含まれる値が変更された時に再計算される const memoizedValue = useMemo < T >(() => computeExpensiveValue ( a , b ), [ a , b ] ); useMemo を使う ユースケース 基本的には最適化のためのフックですが、例えば配列を保持する state で配列を走査する処理が頻繁に必要な場合などに役立ちます。 ToDo リストの例で、一覧から完了済みのアイテムを useMemo によって取得することを考えてみましょう。 const [ todoItems , setToDos ] = useState < ToDo [] >( [] ); const completedItems = useMemo < ToDo [] >(() => { return todoItems.filter (( todoItem ) => todoItem.completed ); } , [ todos ] ); useMemo では依存配列に渡された state が更新された時にメモ化していた値を再計算します。 useCallback useCallback は関数をメモ化するフックです。 useCallback は最適化のためのフックという位置付けです。 そして useCallback を利用する場合は、基本的に React.memo と併用する必要があります。 React.memo と useCallback useCallback の話をする前に、 React.memo について簡単に説明します。 React の最上位 API - React.memo, https://ja.reactjs.org/docs/react-api.html#reactmemo すでに述べた通り、React では親 コンポーネント から子 コンポーネント に props を通してデータを渡します。 通常では、図中の点線で示した子 コンポーネント は親 コンポーネント が再描画されるタイミングで常に再描画されます。 React.memo はこの親 コンポーネント が再描画されるタイミングでの子 コンポーネント の再描画を最適化するものです。 React.memo では子 コンポーネント において、親 コンポーネント から受け取る props が再描画前の props と等価であれば、再描画をスキップします。つまり親 コンポーネント から子 コンポーネント に渡す props とその等価性が重要になります。 useCallback は props に渡す関数が等価であることを保証するためのフックです。 useCallback を使うための基本的なコードは以下のとおりです。 // 返り値はメモ化された関数 // 第2引数の依存配列に含まれる値が変更された時に再計算される const memoizedCallback = useCallback (() => { doSomething ( a , b ); } , [ a , b ] ); これにより子 コンポーネント では、props 受け取った関数が useCallback の第2引数の依存配列に含まれる値が変更されていない限りは等価なものとして扱えます。 useCallback を使う ユースケース ここまで説明した通り、 useCallback は最適化の流れで、子 コンポーネント の props に関数を渡す必要が生じた際に利用します。 React.memo は使用しませんが、props に関数を渡す場面を先ほどのログインフォームの例で見てみましょう。 まずフォームの状態を useState を使って定義していました。 const [ formData , setFormData ] = useState < SampleLoginForm >( { userId: "" , password: "" , } ); そして input 要素の onChange プロパティに関数を記述し formData の値を更新していました。 < input id = "userId" type= "text" name = "userId" placeholder = "ユーザーID" value = { formData.userId } onChange = { ( e ) => setFormData ( { ...formData , userId: e.target.value } ) } / > この onChange プロパティに渡す関数を useCallback で記述すると以下のようになります。 // inputタグのonChangeイベントを処理する関数 const onChangeHandler = useCallback (( e: ChangeEvent < HTMLInputElement >) => { setFormData (( prev: SampleLoginForm ) => { return { ...prev , [ e.target.name ] : e.target.value } ; } ); } , [] ); useCallback では第 2 引数の依存配列に含まれる値が変更されたタイミングで再度メモ化されるため、依存配列に含まれる値が少なくなるように意識する必要があります。 この例では、更新時に formData を参照せず、 setFormData 関数内で直前の formData の値を受けることよって依存配列が空になるようにしています。 これにより onChangeHandler 関数はメモ化が働き、 React.memo と併用した最適化ができます。 React Hooks を正しく使うために フックは一見すると JavaScript の関数ですが、正しく使う際には、ルールに従う必要があります。 特に useEffect や useMemo 、 useCallback といった依存配列を含むフックの使用では、依存関係の漏れによってバグを混入する恐れがあります。 フックを正しく利用するために、ESLint の eslint-plugin-react-hooks プラグイン を導入しておくことがお勧めです。 exhaustive-deps ルールを有効にすれば、依存配列が正しく記述されていない場合に警告を出すこともできます。 eslint-plugin-react-hooks https://www.npmjs.com/package/eslint-plugin-react-hooks おわりに 本記事では、React で提供される基本的な React Hooks をその ユースケース とともに紹介しました。 ここでは紹介できなかった React Hooks やカスタムフック、テスト方法なども今後、紹介できればと思います。 明日(12/14)は Toshihiro Nakamura さんから「Kotlinでデータベースアクセス」の記事が公開される予定です。 そちらもぜひご覧ください。 執筆: @yamada.y 、レビュー: @sato.taichi ( Shodo で執筆されました )