Redux ToolkitとHooks APIを利用してReduxを導入

「Redux Toolkit( createSlice configureStore )」と「Hooks API( useSelector useDispatch )」を利用したケースで、ReduxをReactに導入する方法について確認します。

前準備

create-react-app でReactアプリを作成して、必要なパッケージをインストールします。
(TypeScriptで動作確認します。)

npx create-react-app my-app --template typescript
cd my-app/
yarn add redux react-redux @reduxjs/toolkit 
yarn add --dev @types/react-redux

フォルダ構造

以下フォルダ構成で構築したReactアプリにて動作確認をしました。

src/
├── components
│   └── Sample.tsx  // 「Hooks API(useDispatch, useSelector)」 と 「各SliceのAction Creator」 で 「Store」 に接続
├── features
│   ├── counter.ts  // Redux Toolkitの 「createSlice」 で 「State, Action Creator, Reducer」 をまとめて作成 
│   └── log.ts      // Redux Toolkitの 「createSlice」 で 「State, Action Creator, Reducer」 をまとめて作成
├── App.tsx         
├── index.tsx       // React Reduxの 「Providerコンポーネント」 を利用して、ReactアプリでReduxを利用できるようにする
└── store.ts        // 「各SliceのReducer」 をまとめて 「Store」 を生成

Redux ToolkitのcreateSlice
( State Action Creator Reducer を作成 )

Redux Toolkitの createSlice を利用すると State Action Creator Reducer をまとめて作成することができます。

757-react-redux-toolkit-hooks_slice_00.png

今回は2つのsliceを作成します。

src/features/counter.ts

import { createSlice } from '@reduxjs/toolkit';

export type CounterState = {
  count: number;
};

const initialState: CounterState = { count: 0 };

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    incrementCount: (state) => {
      return { ...state, count: state.count + 1 };
    },
    decrementCount: (state) => {
      return { ...state, count: state.count - 1 };
    },
  },
});

src/features/log.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

type Log = {
  id: number;
  text: string;
};

export type LogState = {
  logs: Log[];
  loading: boolean;
};

const initialState: LogState = {
  logs: [],
  loading: false,
};

export const logSlice = createSlice({
  name: 'log',
  initialState,
  reducers: {
    addLog: (state, action: PayloadAction<Log>) => {
      return {
        ...state,
        logs: [...state.logs, action.payload],
        loading: false,
      };
    },
    deleteLog: (state, action: PayloadAction<Pick<Log, 'id'>>) => {
      return {
        ...state,
        logs: state.logs.filter((log) => log.id !== action.payload.id),
        loading: false,
      };
    },
    setLogLoading: (state) => {
      return {
        ...state,
        loading: true,
      };
    },
  },
});

Redux ToolkitのconfigureStore
( Storeを作成 )

src/store.ts

combineReducers で各Sliceのreducerをまとめた後、configureStore でStoreを作成します。

import { combineReducers } from 'redux';
import { configureStore } from '@reduxjs/toolkit';

import { counterSlice, CounterState } from 'features/counter';
import { logSlice, LogState } from 'features/log';

export type AppState = {
  counter: CounterState;
  log: LogState;
};

const rootReducer = combineReducers<AppState>({
  counter: counterSlice.reducer,
  log: logSlice.reducer,
});

const store = configureStore({ reducer: rootReducer });

export default store;

React Reduxの Provider
( ReactアプリでReduxを利用 )

src/index.tsx

React Reduxの Provider を利用して、ReactアプリでReduxを利用できるようにします。

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import App from 'App';
import store from 'store';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root'),
);

Hooks API( useDispatch useSelector )
( ComponentからStoreに接続 )

以前は、React Reduxの connect を呼び出し、Storeに接続していました。

Hooks API( useDispatch useSelector )を利用してStoreに接続できるようになりました。

src/App.tsx

import React, { FC } from 'react';

import Sample from 'components/Sample';

const App: FC = () => {
  return (
    <div style={{ padding: '12px', width: '400px', backgroundColor: '#ccc' }}>
      <h1>App</h1>
      <Sample samplePropData="Data passed from parent." />
    </div>
  );
};

export default App;

src/components/Sample.tsx

import React, { FC } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { counterSlice } from 'features/counter';
import { logSlice, LogState } from 'features/log';
import { AppState } from 'store';

type Props = {
  samplePropData?: string;
};

const Sample: FC<Props> = ({ samplePropData }) => {
  const { count, log } = useSelector<
    AppState,
    { count: number; log: LogState }
  >((state) => ({
    count: state.counter.count,
    log: state.log,
  }));
  const dispatch = useDispatch();
  const { incrementCount, decrementCount } = counterSlice.actions;
  const { addLog, deleteLog, setLogLoading } = logSlice.actions;

  return (
    <div style={{ padding: '12px', backgroundColor: '#eee' }}>
      <h1>Sample Component</h1>
      <p>samplePropData: {samplePropData}</p>
      <h2>Counter State</h2>
      <p>count: {count}</p>
      <button type="button" onClick={() => dispatch(incrementCount())}>
        incrementCount
      </button>
      <button type="button" onClick={() => dispatch(decrementCount())}>
        decrementCount
      </button>
      <h2>Log State</h2>
      <p>log.logs.length: {log.logs.length}</p>
      <p>log.loading: {log.loading.toString()}</p>
      <button
        type="button"
        onClick={() => dispatch(addLog({ id: 1, text: 'xxx' }))}
      >
        addLog
      </button>
      <button type="button" onClick={() => dispatch(deleteLog({ id: 1 }))}>
        deleteLog
      </button>
      <button type="button" onClick={() => dispatch(setLogLoading())}>
        setLogLoading
      </button>
    </div>
  );
};

export default Sample;
  • 状態の取得
    • useSelector でStoreから状態を取得します。
  • 状態の更新
    • 各SliceからAction Creator関数を取得してます。
    • useDispatch の戻り値で取得した関数を経由してActionをdispatchしてます。

動作確認

Redux Devtools を利用して動作確認します。

757-react-redux-toolkit-hooks_devtool.gif

参考