LoginSignup
14
7

More than 1 year has passed since last update.

【React】useContext✖️useReducerでグローバルな状態管理を実現する。

Posted at

まえがき

ReactフックのuseContextuseReducerを組み合わせて使えば、ReduxRecoil等のライブラリを使わずにグローバル状態管理を実現することができる。本稿ではそれを実装するために必要な知識とコード例をまとめる。

参考記事

下記の記事がとても分かりやすくまとまっていました👏

前提

今回扱うuseContextuseReducerReactフックと呼ばれるものである。他にもいろいろある。

useContext
1箇所に定義されているデータを、複数箇所(コンポーネント)からグローバルに参照できるようにするReactフック(関数)。参照できるようにするだけであって、更新はできない。

useReducer
複雑な状態管理を実現するもの。単純な状態管理はuseStateで事足りる。

Reactフックとは

状態管理やライフサイクルのReact機能を、クラスを書かずに使えるようになる機能(関数)のこと。
 ・Reactフックが登場する前までは、状態管理/ライフサイクルの機能を扱えるのはクラスコンポーネントだけであった。
 ・Reactフックが登場してからは、関数コンポーネントでも状態管理/ライフサイクルの機能を扱えるようになった。

関数コンポーネント vs クラスコンポーネント

下記のQlita記事、React公式にも記載してある通り、コード量が少なく読みやすい関数コンポーネントを使う。

useContext

1箇所に定義されているデータを、複数箇所(コンポーネント)からグローバルに参照できるようにするReactフック(関数)。参照できるようにするだけであって、更新はできない。

使い方

親コンポーネント(App.tsx)に定義したデータを、複数の子コンポーネントから参照できるようにする。

App.tsx(親コンポーネント)
import React, { createContext } from "react";
// 子コンポーネントを3つ用意。
import { AccountListSection } from "./accounts/AccountListSection";
import { MailListSection } from "./mails/MailListSection";
import { MessageListSection } from "./messages/MessageListSection";

type DataStoreContext = {
  accounts: string[];
  mails: string[];
  messages: string[];
};
// 参照できるようにするContextの初期値
const initialContext: DataStoreContext = {
  accounts: [],
  mails: [],
  messages: [],
};
// Context作成&子コンポーネントから参照できるようにexportする。
export const DataStoreContext = createContext(initialContext);

const App = (): JSX.Element => {
  // 参照できるようにするContextのオブジェクト
  const contextState = {
    accounts: ["アカウントA", "アカウントB", "アカウントC"],
    mails: ["メールA", "メールB", "メールC"],
    messages: ["メッセージA", "メッセージB", "メッセージC"],
  };

  return (
    <div>
      <DataStoreContext.Provider value={contextState}>
        <AccountListSection />
        <MailListSection />
        <MessageListSection />
      </DataStoreContext.Provider>
    </div>
  );
}
accounts/AccountListSection.tsx(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../App";

export const AccountListSection = (): JSX.Element => {
  // 親(App.tsx)がexportしているDataStoreContextをINPUTに、useContextを実行。
  const dataStoreContext = useContext(DataStoreContext);
  return (
    <div>
      <h1>AccountListSection</h1>
      <p>{dataStoreContext.accounts.join(", ")}</p>
    </div>
  );
};
mails/MailListSection.tsx(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../App";

export const MailListSection = (): JSX.Element => {
  // 親(App.tsx)がexportしているDataStoreContextをINPUTに、useContextを実行。
  const dataStoreContext = useContext(DataStoreContext);
  return (
    <div>
      <h1>MailListSection</h1>
      <p>{dataStoreContext.mails.join(", ")}</p>
    </div>
  );
};
messages:MessageListSection(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../App";

export const MessageListSection = (): JSX.Element => {
  // 親(App.tsx)がexportしているDataStoreContextをINPUTに、useContextを実行。
  const dataStoreContext = useContext(DataStoreContext);
  return (
    <div>
      <h1>MessageListSection</h1>
      <p>{dataStoreContext.messages.join(", ")}</p>
    </div>
  );
};

起動するとこんな画面が表示される。
image.png

useReducer

・状態管理をするためのReactフック。状態管理をする点でuseStateと同じ役割。
useStateuseReducerの違うところはどうやってstateを更新するか。

useStateでの状態管理

・参照:直接stateオブジェクト(contactState)を参照する。
・更新:state更新用関数(setContactState)に、値を直接を格納する。

import { useState } from "react";

const initialContactState = {
  messages: [],
  mails: [],
}

const [contactState, setContactState] = useState(initialContactState),

// 参照
console.log(`messages: ${contactState.messages}`;
console.log(`mails: ${contactState.mails}`;
// 更新
setContactState({...contactState, messages: ["NEW MESSAGES"]});
setContactState({...contactState, mails: ["NEW MAILS"]});

useReducerでの状態管理

・参照:直接stateオブジェクト(contactState)を参照する。
・更新:state更新用関数(dispatch)をコール with 更新するために必要な情報(type/payload)
    更新処理自体が行われるのはreducer関数内。type/payloadをINPUTに複雑なロジックを
    組みたい場合はこのreducer関数内に書いていく。

import { useState } from "react";

type ContactState = {
  messages: string[];
  mails: string[]
}

// Reducer関数
// ・受け取った引数を元に、stateを更新する。
//  第1引数: 更新前のstate
//  第2引数: dispatch関数の引数経由で送られたオブジェクト
const reducerFunc = (state, action) => {
  switch(action.type) {
    case "FETCH_MESSAGES":
      return {
         ...state,
         messages: action.payload
      };
    case "FETCH_MAILS":
      return {
         ...state,
         mails: action.payload
      };
    default:
      return state;
  }
}
// stateの初期値
const initialContactState = {
  messages: [],
  mails: [],
}

// useReducer()で、参照用のcontactState と 更新用のdispatch関数 を生成。
const [contactState, dispatch] = useReducer(reducerFunc, initialContactState);

// 参照
console.log(`messages: ${contactState.messages}`;
console.log(`mails: ${contactState.mails}`;
// 更新
dispatch({type: "FETCH_MESSAGES", payload: ["NEW MESSAGES"]});
dispatch({type: "FETCH_MAILS", payload: ["NEW MAILS"]});

useStateとuseReducerの使い所

両方とも状態管理をするためのReactフック。それぞれどのタイミングで使うべきか。

Reactフック 使い所
useState ・シンプルな状態を管理するとき(直接値を更新するようなもの)
useReducer ・状態管理するために複雑なロジックが必要になるもの
・Redux,Recoil等を使わずにグローバルな状態管理を実現したい場合

Reactでは、グローバルな状態管理を実現するライブラリとしてReduxRecoilが存在する。
しかし、これらのライブラリを利用せずとも、今まで説明にあげたuseReduceruseContextの2つのReactフックを組み合わせればグローバルな状態管理を実現することができる。

useContext✖️useReducerを組み合わせて使う。

実現したいこと
① APIで取得したデータを1箇所で管理する。
② 複数のコンポーネントから、①を参照&更新できるようにする。

useContextとuseReducerをどう使う?
・useReducerで「参照用のstateオブジェクト」と「更新用のdispatch関数」を生成する。
・useContextで「参照用のstateオブジェクト」と「更新用のdispatch関数」を各コンポーネントからグローバルに参照できるようにする。

useContextでは状態を参照することしかできなかったが、更新用のdispatch関数を参照させることで、各コンポーネントから「状態の参照&状態の更新」を実現できるようにする。

context/DataStoreContext.tsx
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */

import React, { useReducer, createContext } from "react";

// useReducerで生成する「参照用のstate」の型
type DataStore = {
  isLoading: boolean;
  isError: boolean;
  accounts: string[];
  mails: string[];
  messages: string[];
};
// dispatch関数の第2引数に渡す「action」の型
type ReducerAction = {
  type: string;
  payload: any;
};
// createContext()のデフォルト値オブジェクトにasで割り当てる。
type DataStoreContext = {
  state: DataStore;
  // dispatchの引数オブジェクトの型を、React.Dispatch<XXXXX> に定義する。
  dispatch: React.Dispatch<ReducerAction>;
};

// reducer関数:更新用dispatchトリガーで、stateを更新する処理。
// 引数:   1.state 2.action(dispatch関数の引数)
// 戻り値: 更新後の新しいstate
const reducerFunc = (state: DataStore, action: ReducerAction) => {
  // action.typeの値で更新内容を切り替える。
  switch (action.type) {
    case "FETCH_ACCOUNTS":
      return {
        ...state,
        isLoading: true,
        accounts: action.payload,
      };
    case "FETCH_MAILS":
      return {
        ...state,
        isLoading: true,
        mails: action.payload,
      };
    case "FETCH_MESSAGES":
      return {
        ...state,
        isLoading: true,
        messages: action.payload,
      };
    // 更新前のstateをそのまま返す。
    default:
      return state;
  }
};
const initialState: DataStore = {
  isLoading: false,
  isError: false,
  accounts: ["INITIAL ACCOUNT"],
  mails: ["INITIAL_MAIL"],
  messages: ["INITIAL_MESSAGE"],
};

// createContextはReactフックではないため、コンポーネント外で使用可能
// as でオブジェクトの型チェックをクリアする。
export const DataStoreContext = createContext({} as DataStoreContext);

export const DataStoreContextProvider = (props): JSX.Element => {
  // useReducerで生成した「参照用state」と「更新用dispatch」を、contextに渡す。
  const [state, dispatch] = useReducer(reducerFunc, initialState);
  return (
    <DataStoreContext.Provider
      value={{
        state,
        dispatch,
      }}
    >
      {props.children}
    </DataStoreContext.Provider>
  );
};
App.tsx(親コンポーネント)
import { DataStoreContextProvider } from "./context/DataStoreContext";
import { AccountListSection } from "./accounts/AccountListSection";
import { MailListSection } from "./mails/MailListSection";
import { MessageListSection } from "./messages/MessageListSection";

const App = (): JSX.Element => {
  return (
    <div>
      <DataStoreContextProvider>
        <AccountListSection />
        <MailListSection />
        <MessageListSection />
      </DataStoreContextProvider>
    </div>
  );
};

export default App;
accounts/AccountListSection.tsx(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../context/DataStoreContext";

export const AccountListSection = (): JSX.Element => {
  const { state, dispatch } = useContext(DataStoreContext);
  const getAccounts = () => {
    // useEffect関数内でAPIコール -> dispatchのpayloadに渡す。
    // (動作検証用なので今回はbuttonクリックをトリガーにする)
    dispatch({ type: "FETCH_ACCOUNTS", payload: ["NEW ACCOUNTS"] });
  };
  return (
    <div>
      <h1>AccountListSection</h1>
      <p>{state.accounts.join(", ")}</p>
      <button onClick={getAccounts}>GET NEW ACCOUNTS</button>
    </div>
  );
};
mails/MailListSection.tsx(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../context/DataStoreContext";

export const MailListSection = (): JSX.Element => {
  const { state, dispatch } = useContext(DataStoreContext);

  const getMails = () => {
    // useEffect関数内でAPIコール -> dispatchのpayloadに渡す。
    // (動作検証用なので今回はbuttonクリックをトリガーにする)
    dispatch({ type: "FETCH_MAILS", payload: ["NEW MAILS"] });
  };

  return (
    <div>
      <h1>MailListSection</h1>
      <p>{state.mails.join(", ")}</p>
      <button onClick={getMails}>GET NEW MAIL</button>
    </div>
  );
};
messages/MessageListSection.tsx(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../context/DataStoreContext";

export const MessageListSection = (): JSX.Element => {
  const { state, dispatch } = useContext(DataStoreContext);
  const getMessages = () => {
    // useEffect関数内でAPIコール -> dispatchのpayloadに渡す。
    // (動作検証用なので今回はbuttonクリックをトリガーにする)
    dispatch({ type: "FETCH_MESSAGES", payload: ["NEW MESSAGES"] });
  };
  return (
    <div>
      <h1>MessageListSection</h1>
      <p>{state.messages.join(", ")}</p>
      <button onClick={getMessages}>GET NEW MESSAGES</button>
    </div>
  );
};

アプリ起動
image.png
それぞれのボタンをクリックすると、dispatch関数で状態が更新される。
image.png

余談

スプレッド構文(...state)

const initialState = {
  name: "UNKNOWN",
  age: 0,
  birthDate: "2000-01-01",
};

console.log({
  ...initialState,
  name: "山田太郎",
  age: 18,
});

// 出力結果 (nameとageは上書きされ、birthDataはそのまま)
{ name: '山田太郎', age: 18, birthDate: '2000-01-01' }
14
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7