はじめに
表題通り、Reactでfluxのデザインパターンを実装していきます。
成果物
題材はカウンターです。+1ボタンを押すとインクリメントされ、リセットボタンを押すとリセットされるよくあるカウンターの実装です。
Hands-on
ディレクトリ構成
~/develop/react/react_flux$ tree -I node_modules
.
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.tsx
│ ├── Counter.tsx
│ ├── commons
│ │ ├── Store.tsx
│ │ ├── actions.ts
│ │ ├── counterReducer.ts
│ │ ├── reducer.ts
│ │ └── state.ts
│ ├── index.tsx
│ └── logo.svg
├── tsconfig.json
└── yarn.lock
4 directories, 19 files
index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
src/App.tsx
import React, { useReducer } from "react";
import { reducer } from "./commons/reducer";
import { initialState } from "./commons/state";
import Store, { StoreContext } from "./commons/Store";
import Counter from "./Counter";
function App() {
// state は現在の状態を表す変数、dispatch は状態を変更するための関数
const [state, dispatch] = useReducer(reducer, initialState);
// StoreContext は、state と dispatch を保持するコンテキスト
const context: StoreContext = {
state,
dispatch,
};
return (
<Store.Provider value={context}>
<Counter />
</Store.Provider>
);
}
export default App;
src/Counter.tsx
import React, { useContext } from "react";
import Store from "./commons/Store";
import { COUNTER_INCREMENT, COUNTER_RESET } from "./commons/actions";
export default function Counter() {
// useContext を使って StoreContext から state と dispatch を取得
const context = useContext(Store);
const { state, dispatch } = context;
const increment = () => dispatch({ type: COUNTER_INCREMENT });
const reset = () => dispatch({ type: COUNTER_RESET });
return (
<div>
<h1>現在のカウンター値:{state.counter.count}</h1>
<button onClick={increment}>+1</button>
<button onClick={reset}>リセット</button>
</div>
);
}
src/commons/actions.ts
export const COUNTER_INCREMENT = "INCREMENT";
export const COUNTER_RESET = "RESET";
type CounterActionTypes = typeof COUNTER_INCREMENT | typeof COUNTER_RESET;
type ActionTypes = CounterActionTypes;
export type Action = {
type: ActionTypes;
};
src/commons/counterReducer.ts
import type { Reducer } from "react";
import { COUNTER_INCREMENT, type Action, COUNTER_RESET } from "./actions";
import type { GlobalState } from "./state";
import { initialState } from "./state";
export const counterReducer: Reducer<GlobalState["counter"], Action> = (
state,
action
) => {
switch (action.type) {
case COUNTER_INCREMENT: {
const count = state.count + 1;
return {
...state,
count,
};
}
case COUNTER_RESET: {
return { ...initialState.counter };
}
default:
return state;
}
};
src/commons/reducer.ts
import type { Reducer } from "react";
import type { Action } from "./actions";
import type { GlobalState } from "./state";
import { counterReducer } from "./counterReducer";
export const reducer: Reducer<GlobalState, Action> = (state, action) => {
return {
action,
counter: counterReducer(state.counter, action),
};
};
src/commons/state.ts
import type { Action } from "./actions";
export type GlobalState = {
action: Action | null;
counter: CounterState;
};
type CounterState = {
count: number;
};
export const initialState: GlobalState = {
action: null,
counter: {
count: 0,
},
};
src/commons/Store.tsx
import type { GlobalState } from "./state";
import type { Action } from "./actions";
import React from "react";
export type StoreContext = {
state: GlobalState;
dispatch: (action: Action) => void;
};
const Store = React.createContext({} as StoreContext);
export default Store;