こんにちは、かつコーチです。
前回は、Context APIを使って実践的なグローバル状態を作る方法を解説しました。
Context APIはアプリ全体の状態を共有するのに便利ですが、状態の種類が増えたり、更新ロジックが複雑になったりすると、だんだん管理が大変になってきます。
そこで登場するのが、状態管理ライブラリの定番であるRedux Toolkitです。
「Reduxって難しそう」というイメージを持っている人も多いと思いますが、Redux Toolkitは従来のReduxの面倒な部分を大幅に削ってくれています。
今日は、Redux Toolkitの基本的な使い方を、実際に手を動かせるコード例とともに解説していきます。
Redux Toolkitとは?
従来のReduxとの違い
Reduxは、アプリ全体の状態を1つのstore(状態を保管する場所)にまとめて管理するためのライブラリです。
従来のReduxは、Action・Reducer・Storeをそれぞれ手作業でボイラープレート(お決まりの定型コード)として書く必要があり、学習コストが高いことで知られていました。
Redux Toolkit(RTK) は、そのボイラープレートを大幅に削減するために公式が提供している、Reduxの「現在の標準的な使い方」です。
具体的には、次のような機能が最初から組み込まれています。
createSlice:Action・Reducerをまとめて自動生成- Immerというライブラリが内蔵されていて、状態を直接書き換える書き方をしても内部的にイミュータブル(不変)に変換してくれる
- DevToolsやミドルウェアの設定が最初から有効
「Reduxを使うなら基本的にRedux Toolkitを使う」というのが、今のReactエコシステムでは一般的な考え方になっています。
どんな場面で使うのか
Redux Toolkitが向いているのは、次のようなケースです。
- 状態の種類・更新パターンが多く、複雑な業務ロジックを含むアプリ
- 複数人・複数チームで開発する大規模なプロジェクト
- 「いつ・誰が・どんな理由で状態を変更したか」を追跡できるようにしたい場合
逆に、小規模なアプリや個人開発でここまでの仕組みが必要になることは少なく、Context APIや後述するZustandで十分なケースも多いです。
この判断軸については、次々回の比較記事で詳しく整理します。
基本の書き方
storeを作成する
まずはRedux Toolkitをインストールします。
npm install @reduxjs/toolkit react-redux
次に、状態を管理するslice(状態のかたまりを機能ごとに分けた単位)を作成します。
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
type CounterState = {
count: number;
};
const initialState: CounterState = { count: 0 };
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.count += 1; // Immerのおかげで直接書き換えてOK
},
decrement: (state) => {
state.count -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.count += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
createSlice に name・initialState・reducers を渡すだけで、ActionとReducerが自動的に生成されます。
state.count += 1 のように直接状態を書き換えているように見えますが、内部で使われているImmerというライブラリが、裏側で新しいオブジェクトを作る処理に変換してくれています。
前回・前々回で「stateを直接書き換えてはいけない」と説明したルールを、Redux Toolkitでは意識せずに書けるのがポイントです。
storeをまとめてアプリに接続する
作成したsliceをstoreに登録し、アプリ全体に接続します。
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
import { Provider } from "react-redux";
import { store } from "./store";
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
configureStore に reducer として各sliceをまとめて渡すことで、アプリ全体で使う1つの store が完成します。
Provider でアプリ全体を囲むのは、Context APIの Provider と同じ考え方です。
コンポーネントから状態を読み書きする
useSelector と useDispatch を使って、コンポーネントからstoreの状態を読み書きします。
import { useSelector, useDispatch } from "react-redux";
import { RootState, AppDispatch } from "./store";
import { increment, decrement } from "./counterSlice";
function Counter() {
const count = useSelector((state: RootState) => state.counter.count);
const dispatch = useDispatch<AppDispatch>();
return (
<div>
<p>カウント:{count}</p>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
</div>
);
}
useSelector はstoreの中から必要な値だけを取り出し、useDispatch でActionを発行して状態を更新します。
つまずきやすいポイント:useSelectorの範囲指定ミスによる再レンダリング
かつコーチが実際にハマった話
私が初めてRedux Toolkitを実務で使ったとき、useSelector でstate全体をそのまま取得してしまい、関係のない状態が更新されるたびにコンポーネントが再レンダリングされる問題に直面したことがあります。
❌ Before:stateオブジェクト全体をuseSelectorで取得する
function Counter() {
// state全体を取得しているため、counter以外の状態が変わっても再レンダリングされる
const state = useSelector((state: RootState) => state);
const dispatch = useDispatch<AppDispatch>();
return (
<div>
<p>カウント:{state.counter.count}</p>
<button onClick={() => dispatch(increment())}>+1</button>
</div>
);
}
useSelector に渡す関数がstate全体を返すオブジェクトになっているため、store の中のどれか1つの状態が変わるたびに、このコンポーネントの再レンダリングが発生してしまいます。
規模の小さいアプリでは気づきにくいですが、状態の種類が増えるほど、無駄な再レンダリングが積み重なってパフォーマンスに影響が出てきます。
✅ After:必要な値だけを絞ってuseSelectorで取得する
function Counter() {
// counter.countという必要な値だけを取得する
const count = useSelector((state: RootState) => state.counter.count);
const dispatch = useDispatch<AppDispatch>();
return (
<div>
<p>カウント:{count}</p>
<button onClick={() => dispatch(increment())}>+1</button>
</div>
);
}
useSelector は、渡した関数の戻り値が変わったときだけ再レンダリングを発生させる仕組みになっています。
必要な値だけをピンポイントで取り出すよう意識するだけで、無駄な再レンダリングを大きく減らせます。
応用・一歩先の使い方
非同期処理はcreateAsyncThunkで扱う
APIからデータを取得するような非同期処理は、createAsyncThunk を使ってsliceに組み込めます。
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
type User = { id: number; name: string };
export const fetchUser = createAsyncThunk("user/fetch", async (userId: number) => {
const res = await fetch(`/api/users/${userId}`);
return (await res.json()) as User;
});
type UserState = {
data: User | null;
status: "idle" | "loading" | "succeeded" | "failed";
};
const userSlice = createSlice({
name: "user",
initialState: { data: null, status: "idle" } as UserState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.status = "loading";
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = "succeeded";
state.data = action.payload;
})
.addCase(fetchUser.rejected, (state) => {
state.status = "failed";
});
},
});
pending・fulfilled・rejected という3つの状態を extraReducers で扱うことで、通信中・成功・失敗の状態をまとめて管理できます。
RTK Queryでデータ取得をさらに簡略化する
Redux Toolkitには RTK Query というデータ取得専用の機能も含まれています。
キャッシュ管理・再取得・ローディング状態の管理まで自動化してくれるため、createAsyncThunk を手書きするよりもさらに少ないコードでAPI連携を実装できます。
本格的にRedux Toolkitを使う場合は、RTK Queryも合わせて調べてみることをおすすめします。
まとめ
この記事のポイント
- Redux Toolkitは、従来のReduxのボイラープレートを大幅に削減した公式の標準的な使い方
createSliceでAction・Reducerをまとめて生成し、Immerのおかげで直接書き換えるような記法が使えるconfigureStoreとProviderでアプリ全体にstoreを接続するuseSelectorは必要な値だけをピンポイントで取得し、無駄な再レンダリングを防ぐ- 非同期処理は
createAsyncThunkやRTK Queryで扱える
次に読むべき記事
次回は、シンプルな状態管理ライブラリであるZustandの入門編を解説します。
→ 次の記事:Zustand入門:シンプルな状態管理ライブラリ