Reactアプリへの Reduxの導入手順
この記事では、ReduxをReactアプリに導入する手順を書いていきます。
コード自体はTypescriptですが、Javascriptでも同じような手順でできます。
注: 勉強メモ程度に書いているので、説明が結構雑です。
記事内のコードは公開していないインターフェースなども含んでいるので、例程度に見てください。
1. 利用するパッケージのインストール
まずは、redux周りのパッケージをインストールしています。今回は、Reduxの中で非同期な処理を行うことを想定して、redux-thunkというパッケージもインストールします。redux-thunkを使うことで、axiosを使ったwebAPIとの通信など非同期な処理を行うことができるようになります。(ただし、今回の記事の中では非同期通信は行わない。)
(ターミナル)
yarn add react-redux @types/react-redux
yarn add redux
yarn add redux-thunk // 非同期な処理を行いたい場合のみ
2. Redux用のフォルダ整理
コードを書いていく前に、redux周りのコードを入れていくフォルダ構成を作っていきます。srcディレクトリの中に、stateというフォルダを作成して、その中にaction-types, actions, action-creators, reducersという名前のフォルダを作成して、下の図のようなフォルダ構成にしてください。
src/
└ state/
|- action-types/
|- actions/
|- action-creators/
└- reducers/
action-types: アクションのtypeを定義するファイルを入れるフォルダ
actions: アクションのinterfaceを定義するファイルを入れる
action-creators: action creatorのinterfaceを定義するファイルを入れる
reducer: reducerを定義下ファイルを入れるフォルダ
3. アクションタイプの定義
フォルダを作成したら、action-typesの中にindex.tsファイルを作成して、後で作るactionが持つ、type(string)をenumで定義していきます。アクションタイプを定義することで、スペルミスを減らしたり、エディタの予測変換を効かせるようになります。
(action-types/index.ts)
export enum ActionTypes {
SET_MENU = 'set_menu',
SET_INSTRUCTIONS = 'set_instructions',
PUSH_SCORE = 'push_score',
}
4. アクションインターフェースの定義
後に作成するActionCreatorが返すオブジェクトのtypeとpayloadの型をインターフェースで定義していきます。actionsディレクトリの中にindex.tsファイルを作成してinterfaceを書いていきます。
(actions/index.ts)
import { Instruction } from '../../components/Training/Instructions';
import { ActionTypes } from '../action-types';
import Menu from '../menu';
export interface SetMenuAction {
type: ActionTypes.SET_MENU;
payload: Menu;
}
export interface SetInstructionsAction {
type: ActionTypes.SET_INSTRUCTIONS;
payload: Instruction[];
}
export interface PushScoreAction {
type: ActionTypes.PUSH_SCORE;
payload: number;
}
export type Action = SetMenuAction | SetInstructionsAction | PushScoreAction;
一番下でActionをエクスポート。
ちなみに、Menuというinterfaceは、以下のようになっています。
export default interface Menu {
title: string;
timeLimit: number;
numOfInstructions: number;
}
5. Reducerの作成
次は、reduerの方を作っていきます。state管理をしていて、あとで作るaction creatorから受け取ったpayloadを元にstateを更新する役割です。
reducersディレクトリの中に、trainingReducer.tsというファイルを作成します。
まずは、stateのインターフェースとreducerに渡す初期値の定義をします。
(reducers/trainingReducers)
import { Instruction } from '../../components/Training/Instructions';
import { ActionTypes } from '../action-types';
import { Action } from '../actions';
import Menu from '../menu';
interface TrainingState {
menu: Menu;
instructions: Instruction[];
scores: number[];
}
const initialState: TrainingState = {
menu: {
title: 'Nothing',
timeLimit: 0,
numOfInstructions: 0,
},
instructions: [],
scores: [],
};
次にreducerを作成
import { Instruction } from '../../components/Training/Instructions';
import { ActionTypes } from '../action-types';
import { Action } from '../actions';
import Menu from '../menu';
interface TrainingState {
menu: Menu;
instructions: Instruction[];
scores: number[];
}
const initialState: TrainingState = {
menu: {
title: 'Nothing',
timeLimit: 0,
numOfInstructions: 0,
},
instructions: [],
scores: [],
};
// ↓new
const reducer = (state: TrainingState = initialState, action: Action): TrainingState => {
switch (action.type) {
case ActionTypes.SET_MENU:
return { ...state, menu: action.payload };
case ActionTypes.SET_INSTRUCTIONS:
return { ...state, instructions: action.payload };
case ActionTypes.PUSH_SCORE:
let newScores = state.scores;
newScores.push(action.payload)
return { ...state, scores: newScores };
default:
return state;
}
};
export default reducer;
TrainingReducerを作り終わったので、次はindex.tsファイルをreducersフォルダに作成して、他のreducerと統合します。(今回は他のreducrはないですが、ある場合はここで統合する。)
(reducers/index.ts)
import { combineReducers } from "redux";
import cellsReducer from './cellsReducer';
import bundlesReducer from './bundlesReducer';
const reducers = combineReducers({
cells: cellsReducer,
bundles: bundlesReducer,
});
export default reducers;
export type RootState = ReturnType<typeof reducers>;
最後に、RootStateをエクスポートしているのは、React側でreducerで管理しているstateの型を確認できるようにするためです。reducerの作成が終わったので、次はaction creatorの方を作っていきます。
6. action creatorsの作成
action creatorはreducerで管理しているデータを更新するときに使うデータと、なんのデータを更新するのかを指定するのが役割です。なんのデータを更新するのかを表すtypeと、更新時に使うデータを積んだpayloadを返す関数を作っていきます。
action-creatorsフォルダの中にindex.tsを新規作成してください。
import { Instruction } from '../../components/Training/Instructions';
import { ActionTypes } from '../action-types';
import {
SetMenuAction,
SetInstructionsAction,
PushScoreAction,
} from '../actions';
import Menu from '../menu';
export const setMenu = (menu: Menu): SetMenuAction => {
return {
type: ActionTypes.SET_MENU,
payload: menu,
};
};
export const setInstructions = (
instructions: Instruction[]
): SetInstructionsAction => {
return {
type: ActionTypes.SET_INSTRUCTIONS,
payload: instructions,
};
};
export const pushScore = (score: number): PushScoreAction => {
return {
type: ActionTypes.PUSH_SCORE,
payload: score,
};
};
これでaction creatorの方も終わりです。
7. storeの作成とreactにreduxを積む作業
stateを管理するstoreを作成していく。stateフォルダの下にstore.tsを作成して、以下のようにしてください。
import { createStore } from "redux";
import reducers from "./reducers";
import reducer from "./reducers/trainingReducer";
export const store = createStore(reducer);
redux-thunkを使う場合は下のようにしてください。
import { createStore } from "redux";
import reducers from "./reducers";
export const store = createStore(reducers);
storeを作れたので、reactの方にstoreを積んでいきます。reduxを使いたいところ全てのJSXを囲うように、Providerを設置します。
import { Provider } from 'react-redux';
const App = () => {
return (
<Provider store={store}>
<div>
<reduxを使いたいところ/>
</div>
</Provider>
);
};
最後に、stateフォルダないのコードを呼び出しやすくするため、ちょっとした作業をします。
stateフォルダにindex.tsファイルを作成して、以下のようにしてください。
export * from './store';
export * from './reducers';
export * from './cell';
export * as actionCreators from './action-creators';
これでredux側の準備は全部終わったので、次はreactの方からreduxを使うコードを書いていきます。
8. カスタムフックの作成
reactの方でreduxを使いやすくするため、actionを発生させるuseActionsフックと、reduxのstateにアクセスするuseTypedSelectorフックを作ります。
まずは、useActionsから。
srcフォルダの下に、hooksという名前のフォルダを作り、その中にuseActions.tsという名前のファイルを作成してください。
import { useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../state';
export const useActions = () => {
const dispatch = useDispatch();
return bindActionCreators(actionCreators, dispatch);
};
reactの中では、useActionsの中に格納されているaction creatorを呼び出してactionを発生させます。
では、useTypedSelectorの方も作っていきます。
hooksディレクトリの中に、useTypedSelector.tsというファイルを作成して以下のように書いてください。
import { useSelector, TypedUseSelectorHook } from "react-redux";
import { RootState } from "../state";
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
react側ではこれを使って、reduxのstateにアクセスします。
導入手順としては以上で終了です。使用例を最後にのっけておきます。
9. 使用例
Trainingというreactコンポーネントで使用。
import { useActions } from '../../hooks/useActions';
import { useTypedSelector } from '../../hooks/useTypedSelector';
const Training = () => {
// get actionCreators and redux state
const { setMenu, setInstructions,pushScore } = useActions();
const { menu, instructions, scores } = useTypedSelector((state) => {
return {
menu: state.training.menu,
instructions: state.training.instructions,
scores: state.training.scores,
}
});
// setup
useEffect(() => {
setMenu({
title: 'Test Menu',
timeLimit: 100000,
numOfInstructions: 10,
});
}, []);
return (
<h1>{menu.title}</h1>
);
}
自分のコードの場合、下のように表示されます。