ReduxToolkitとは、Reactの状態管理を行なうReduxを簡単に記述するためのライブラリである。StackBlitz上でReduxToolkitを使ってみることにした。npx create-react-app my-app --template redux-typescript
で作成されるカウンターを参考にする。
プロジェクト作成
- ダッシュボードにて
React TypeScript
のアイコンをクリックするとプロジェクトが作成作成される。 DEPENDENCIES
のEnter package name
にreact-redux
と入力し、react-reduxをインストールする。- 同様に
@reduxjs/toolkit
をインストールする。 - 以下の手順で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.tsx
にProvider
を追加する。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され、その間カウントアップボタンが日活性になっている。
コメント