StackBlitzでReduxToolkitを使ってみる

React

ReduxToolkitとは、Reactの状態管理を行なうReduxを簡単に記述するためのライブラリである。StackBlitz上でReduxToolkitを使ってみることにした。
npx create-react-app my-app --template redux-typescriptで作成されるカウンターを参考にする。

プロジェクト作成

  1. ダッシュボードにてReact TypeScriptのアイコンをクリックするとプロジェクトが作成作成される。
  2. DEPENDENCIESEnter package namereact-reduxと入力し、react-reduxをインストールする。
  3. 同様に@reduxjs/toolkitをインストールする。
  4. 以下の手順でstore, hooks, sliceを作成する。

Store作成

  • src/appフォルダを作成し、store.tsを作成する。
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: {},
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

hooks作成

  • src/appフォルダに、hooks.tsを作成する。
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Slice作成

  • src/features/counterフォルダに、counterSlice.tsを作成する。
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState, AppThunk } from '../../app/store';

export interface CounterState {
  value: number;
  isLoading: boolean;
  status: 'idle' | 'loading' | 'failed';
}

const initialState: CounterState = {
  value: 0,
  status: 'idle',
  isLoading: false,
};

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {},
  extraReducers: (builder) => {},
});

export default counterSlice.reducer;

ReactにRedux storeをProvedeする

index.tsxProviderを追加する。AppコンポーネントをProviderで囲むことで、Appコンポーネントで読み込まれるすべてのコンポーネントでstoreを使用することが可能になる。

import * as React from 'react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { store } from './src/app/store'; // 追加
import { Provider } from 'react-redux'; // 追加

import App from './App';

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <Provider store={store}> {/* 追加 */}
      <App />
    </Provider> {/* 追加 */}
  </StrictMode>
);

コンポーネント作成

せっかくReduxToolkitを使用するので、サンプルにあるような同一ページではなく、カウントアップするボタンとカウントを表示するコンポーネントを分けてみる。

tsconfig.json

  • import React from 'react'の形式でインポートすると、Module '"stackblitz:/node_modules/@types/react/index"' can only be default-imported using the 'esModuleInterop' flag(1259)という警告が出るので、tsconfig.jsonを以下の内容で作成する。
{
  "compilerOptions": {
    "esModuleInterop": true
  }
}

ボタンコンポーネント作成

  • src/components/Button/Button.tsxを作成する。
import React from 'react';

const Button = () => {
  return (
    <React.Fragment>
      <button>カウントアップ</button>
    </React.Fragment>
  );
};

export default Button;

カウント表示コンポーネント作成

  • src/components/Counter/Counter.tsxを作成する。
import React from 'react';

const Counter = () => {
  return (
    <React.Fragment>
      <div>カウント:</div>
    </React.Fragment>
  );
};

export default Counter;

Appコンポーネントに組み込む

  • App.tsxを以下のように修正する。
import * as React from 'react';
import './style.css';
import Button from './src/components/Button/Button'; // 追加
import Counter from './src/components/Counter/Counter'; // 追加

export default function App() {
  return (
    <div>
      <Button /> {/* 追加 */}
      <Counter /> {/* 追加 */}
      <h1>Hello StackBlitz!</h1>
      <p>Start editing to see some magic happen :)</p>
    </div>
  );
}

とりあえず表示される。

カウントアップボタンとカウントを表示
カウントアップボタンとカウントを表示

ロジック実装

ロジックを実装する

非同期処理

src/features/counter/counterAPI.tsに、setTimeoutを使用して3秒後に値を返却する処理を記述する。(サーバーから値を取得する想定)

export function fetchCount(amount = 1) {
  return new Promise<{ data: number }>((resolve) =>
    setTimeout(() => resolve({ data: amount }), 300)
  );
}

Slice

src/features/counter/counterSlice.tsに、Stateの初期値、StateをReduceするためのcreateSlice、非同期処理のcreateAsyncThunkを記述する。

import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState, AppThunk } from '../../app/store';
import { fetchCount } from './counterAPI';

// stateの型を定義
export interface CounterState {
  value: number;
  isLoading: boolean;
  status: 'idle' | 'loading' | 'failed';
}

// stateの初期値
const initialState: CounterState = {
  value: 0,
  status: 'idle',
  isLoading: false,
};

// 非同期処理を呼び出す関数
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount',
  async (amount: number) => {
    const response = await fetchCount(amount);
    // 返却値は`fulfilled` の action payload
    return response.data;
  }
);

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // reducerを記述する
  reducers: {},
  // 非同期処理はextraReducersに記述する
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
        state.isLoading = true;
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
        state.isLoading = false;
      })
      .addCase(incrementAsync.rejected, (state) => {
        state.status = 'failed';
        state.isLoading = false;
      });
  },
});

export const selectCount = (state: RootState) => state.counter.value;
export const isLoading = (state: RootState) => state.counter.isLoading;

export default counterSlice.reducer;

Storeにreducerを登録する

src/app/store.tsにcounterReducerをインポートし、reducerに追加する。

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice'; // 追加

export const store = configureStore({
  reducer: {
    counter: counterReducer, // 追加
  },
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

useSelectorの設定

カウント表示コンポーネントにcountを表示できるようにする。

import React from 'react';
import { useAppSelector } from '../../app/hooks'; // 追加
import { selectCount } from '../../features/counter/counterSlice'; // 追加

const Counter = () => {
  const counter = useAppSelector(selectCount); // 追加
  return (
    <React.Fragment>
      <div>カウント:{counter}</div>
    </React.Fragment>
  );
};

export default Counter;

useDispatchの設定

ボタンコンポーネントからカウントアップできるように修正する。すると、ボタンクリックで+5カウントアップするようになる。また、読み込み中はボタンを非活性にする。

import React from 'react';
import { useAppSelector, useAppDispatch } from '../../app/hooks'; // 追加
import { incrementAsync, isLoading } from '../../features/counter/counterSlice'; // 追加

const Button = () => {
  const dispatch = useAppDispatch(); // 追加
  const disabled = useAppSelector(isLoading); // 追加

  return (
    <React.Fragment>
      <button disabled={disabled} onClick={() => dispatch(incrementAsync(5))}>
        カウントアップ
      </button>
    </React.Fragment>
  );
};

export default Button;

完成!!
カウントアップボタンをクリックすると3秒後にカウントが+5され、その間カウントアップボタンが日活性になっている。

カウントに結果を表示
カウントに結果を表示

コメント

タイトルとURLをコピーしました