本記事は電通国際情報サービス Advent Calendar 2021の 13 日目の記事です。
執筆者は 2021 年新卒入社の XI 本部 AI トランスフォーメンションセンター所属の山田です。
- はじめに
- React Hooks とは
- useState
- useEffect
- useContext
- useReducer
- useMemo
- 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で執筆されました)