📌

React.ts の状態管理に useContext を使う

2021/03/13に公開

React の状態管理の方法で迷ったので、
選択肢とその特徴をまとめようと思います。

React の状態管理の主な方法

  • useState
  • useContext :round_pushpin:
  • Redux

今回は、useContextを使った状態管理の方法についてまとめていきます。

useContext を使った状態管理のポイント

  • React.createContextで生成した Context を Store として管理する。
  • Store にはReact.useReducerで生成した、StateDispatchを格納する。
  • Content のProvider Componentで囲み、Scope を決定する。
  • Store を利用する際は、React.useContextで取得する。
  • Store に格納したStateから状態を読み取る。
  • Store に格納したDispatchから状態を変更する。

useContext を使った状態管理の例

store

  • React.createContextで生成した Context を Store として管理する。
  • Store にはReact.useReducerで生成した、StateDispatchを格納する。
store.tsx
import * as React from 'react';

/**
 * Itemの型定義
 */
export interface Item
  extends Readonly<{
    name: string;
  }> {}

/**
 * Stateの型定義
 */
export interface State
  extends Readonly<{
    list: Item[];
  }> {}

/**
 * アクションの型定義
 */
export interface Action
  extends Readonly<{
    type: 'SAVE' | 'DELETE';
    payload: Item;
  }> {}

type Reducer = React.Reducer<State, Action>;

export type Dispatch = React.Dispatch<Action>;

/**
 * ストアの型定義
 */
export interface Store
  extends Readonly<{
    /*
     * State
     */
    state: State;
    /**
     * Dispatch
     */
    dispatch: Dispatch;
  }> {}

/**
 * Propsの型定義
 */
interface Props
  extends Readonly<{
    /*
     * Child Elements
     */
    children: React.ReactNode;
  }> {}

/**
 * Default Store
 */
const defaultStore: Store = {
  state: {
    list: [],
  },
  dispatch: (action: Action): void => {},
};

/**
 * Reducer
 */
const reducer: Reducer = (prevState: State, action: Action): State => {
  // 各処理は別ファイルに分割してもよい。
  switch (action.type) {
    case 'SAVE':
      // (ry
      return {...prevState};
    // break;
    case 'DELETE':
      // (ry
      return {...prevState};
    // break;
    default:
      throw new TypeError(`Illegal type of action: ${action.type}`);
    // break;
  }
};

/**
 * Context
 */
export const context: React.Context<Store> = React.createContext<Store>(
  defaultStore
);

/**
 * Provider Component
 */
export const Provider: React.FC<Props> = (props: Props): JSX.Element => {
  const [state, dispatch]: [State, Dispatch] = React.useReducer<Reducer>(
    reducer,
    defaultStore.state
  );

  return (
    <>
      <context.Provider value={{state, dispatch}}>
        {props.children}
      </context.Provider>
    </>
  );
};

Page Component

  • Content のProvider Componentで囲み、Scope を決定する。
Page.tsx
import * as React from 'react';
import List from './List';
import {Provider} from './store';

/**
 * Propsの型定義
 */
interface Props
  extends Readonly<{
    /*
     * Child Elements
     */
    children?: never;
  }> {}

/**
 * Page Component
 */
const Page: React.FC<Props> = (props: Props): JSX.Element => {
  return (
    <Provider>
      <List />
    </Provider>
  );
};

export default Page;

List Component

  • Store を利用する際は、React.useContextで取得する。
  • Store に格納したStateから状態を読み取る。
List.tsx
import * as React from 'react';
import ListItem from './ListItem';
import type {Item, State, Store} from './store';
import {context} from './store';

/**
 * Propsの型定義
 */
interface Props
  extends Readonly<{
    /*
     * Child Elements
     */
    children?: never;
  }> {}

/**
 * List Component
 */
const List: React.FC<Props> = (props: Props): JSX.Element => {
  // StoreからStateを取り出す。
  const {state}: {state: State} = React.useContext<Store>(context);

  return (
    <>
      <ul>
        {state.list.map(
          (item: Item, index: number): JSX.Element => {
            return <ListItem key={index} item={item} />;
          }
        )}
      </ul>
    </>
  );
};

export default List;

List Item Component

  • Store を利用する際は、React.useContextで取得する。
  • Store に格納したDispatchから状態を変更する。
ListItem.tsx
import * as React from 'react';
import type {Dispatch, Item, Store, Action} from '../../store';
import {context} from '../../store';

/**
 * Propsの型定義
 */
interface Props
  extends Readonly<{
    /*
     * Child Elements
     */
    children?: never;
    /**
     * Item
     */
    item: Item;
  }> {}

/**
 * List Item Component
 */
const ListItem: React.FC<Props> = (props: Props): JSX.Element => {
  // StoreからDispatchを取り出す。
  const {dispatch}: {dispatch: Dispatch} = React.useContext<Store>(context);

  // Dispatchに渡すActionを定義しておく。
  const saveAction: Action = {
    type: 'SAVE',
    payload: props.item,
  };

  return (
    <li>
      {props.item.name}
      <button
        onClick={(
          event: React.MouseEvent<HTMLButtonElement, MouseEvent>
        ): void => {
          dispatch(saveAction);
        }}
      >
        SAVE
      </button>
    </li>
  );
};

export default ListItem;

まとめ

  • メリット
    • React 以外のモジュールに依存しない。
    • Provider を置く場所によって Global や任意の範囲のスコープに絞ることができる:mag:
    • prop drilling が少なくてすむ。
  • デメリット
    • Scope を広くしすぎると、どこから変更されているか追いづらくなる
    • 非同期処理には対応出来ていないため、自前でなんとかしないといけない:innocent:
GitHubで編集を提案
株式会社ナンバーフォー

Discussion