📌
React.ts の状態管理に useContext を使う
React の状態管理の方法で迷ったので、
選択肢とその特徴をまとめようと思います。
React の状態管理の主な方法
- useState
- useContext :round_pushpin:
- Redux
今回は、useContextを使った状態管理の方法についてまとめていきます。
useContext を使った状態管理のポイント
- React.createContextで生成した Context を Store として管理する。
- Store にはReact.useReducerで生成した、StateとDispatchを格納する。
- Content のProvider Componentで囲み、Scope を決定する。
- Store を利用する際は、React.useContextで取得する。
- Store に格納したStateから状態を読み取る。
- Store に格納したDispatchから状態を変更する。
useContext を使った状態管理の例
store
- React.createContextで生成した Context を Store として管理する。
- Store にはReact.useReducerで生成した、StateとDispatchを格納する。
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:
Discussion