これまで
第一回: useState
, useRef
第二回: useContext
第三回: useEffect
, useLayoutEffect
, useMemo
, useCallback
今回は、useReducer
です。
ステート管理のキモとなるフックですね。。
前提
- React 16.8以降。なのでHook APIの使用が前提です
- Reactの基本的な説明はしません
- TypeScriptで書いてます
申し訳程度の説明
-
公式には
useState
の代替品とあります。実際、useState
はuseReducer
で実装されています。 -
useReducer
- 構文
-
[state, dispatch] = useReducer(reducer, initialState)
-
- 引数
-
reducer
: 現在のステート値を受け取って、新しいステート値を返す関数 -
initialState
: ステートの初期値
-
- 戻り値
-
state
: ステート値 -
dispatch
:reducer
を実行するための関数
-
- 構文
使ってみる
簡単なチェックボックスを作り、useState
と比較してみます。
useState
import React, { useState } from "react";
export default function Checkbox() {
const [checked, setChecked] = useState(false);
const toggle = () => setChecked(checked => !checked)
return (
<>
<input type="checkbox" checked={checked} onChange={toggle} />
{checked ? "checked" : "not checked"}
</>
);
}
useReducer
import React, { useReducer } from "react";
export default function Checkbox() {
const [checked, toggle] = useReducer((checked) => !checked, false);
return (
<>
<input type="checkbox" checked={checked} onChange={toggle} />
{checked ? "checked" : "not checked"}
</>
);
}
なんかステート更新のロジックが抽象化されただけですね。useState
で十分な気が。。
ちょっと複雑なステートを考えてみる
簡単なユーザ情報を表示させるコンポーネントを考えます。
import React, { useReducer } from "react";
/** 管理したいステート */
type State = {
id: string;
firstName: string;
lastName: string;
city: string;
state: string;
email: string;
admin: boolean;
};
type Actions = Partial<State>;
interface Reducer {
(user: State, newDetails: Actions): State;
}
/** ステートの初期値 */
const firstUser: State = {
id: "0391-3233-3201",
firstName: "Taro",
lastName: "Yamada",
city: "Tokyo",
state: "Shinjuku",
email: "t-yamada@react.com",
admin: false
};
/** ステートを受け取りステートを返す関数 */
const reducer: Reducer = (user, newDetails) => ({ ...user, ...newDetails });
function User() {
const [user, setUser] = useReducer(reducer, firstUser);
const handler = () => setUser({ admin: true });
// もしもuseStateで記述していたら、、、
// const handler = () => setUser({ ...user, admin: true });
return (
<div>
<h1>
{user.firstName} {user.lastName} - {user.admin ? "Admin" : "User"}
</h1>
<p>Email: {user.email}</p>
<p>
Location: {user.city}, {user.state}
</p>
<button onClick={handler}>あなたも今日から管理人!</button>
</div>
);
}
export default function App() {
return <User />;
}
ステート更新にいちいちスプレッド構文を使わなくていいのでスッキリしますね。
が、まだuseState
で事足りそうですよね。。
もっと複雑なステートを考えてみる
前述の例に加え、複数のオブジェクトをステートを管理する場合を考えます。
useContext
とuseReducer
を組み合わせてstore
的にステートを一元管理します。
./src/AppProvider.tsx
import React, { createContext, useContext, useReducer } from "react";
type IUser = {
id: string;
firstName: string;
lastName: string;
city: string;
state: string;
email: string;
admin: boolean;
};
/** 管理したいステート */
type State = {
user: IUser;
login: false;
// other
};
type Actions = Partial<State>;
interface AppContextType {
state: State;
dispatch: React.Dispatch<Actions>;
}
/** ステートの更新関数 */
const reducer = (state: State, actions: Actions) => {
const newState = { ...state };
if(actions.user) newState.user = actions.user
if(actions.login) newState.login = actions.login
return newState;
};
/** ステートの初期値 */
const initState = {} as State;
/** コンテキストをエクスポート */
const AppContext = createContext({} as AppContextType);
export const useAppContext = () => useContext(AppContext);
/** コンテキスト */
const AppContextProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
export default AppContextProvider;
./src/index.tsx
import React from "react";
import { render } from "react-dom";
import AppProvider from "./AppProvider"
import App from "./App";
render(
<AppProvider>
<App />
</AppProvider>,
document.getElementById("root")
);
./src/App.tsx
import React, { useEffect } from "react";
import { useAppContext } from "./AppProvider";
function User() {
const { state, dispatch } = useAppContext();
const { user } = state;
const handler = () =>
dispatch({
user: { ...user, admin: true }
});
useEffect(() => dispatch({ login: true }));
return (
<div>
<h1>
{user.firstName} {user.lastName} - {user.admin ? "Admin" : "User"}
</h1>
<p>Email: {user.email}</p>
<p>
Location: {user.city}, {user.state}
</p>
<button onClick={handler}>あなたも今日から管理人!</button>
</div>
);
}
export default function App() {
return <User />;
}
ステートのスケールを可能にしつつ、一元管理する方法としてはイイ線じゃないでしょうか。。