mcpField
R
redux-toolkit

redux-toolkit

github.com/claude-dev-suite開発4642 回インストール4 日前に更新
リポジトリ

redux-toolkit について

Reduxアプリケーションで状態管理を設定する開発者向けのRedux Toolkitスキルガイドです。slice、store、非同期thunk、型付きフック、RTK Queryに加え、テストやエラー処理も扱います。

主な機能

  1. 01sliceの作成とstore設定
  2. 02thunkによる非同期処理
  3. 03型付きReact Reduxフック
  4. 04RTK Queryでのデータ取得
  5. 05状態管理のベストプラクティス
  6. 06Reduxの一般的な問題の診断

活用シーン

  1. 01型付きアプリケーション状態の設定
  2. 02非同期ユーザーリクエストの実装
  3. 03APIデータとキャッシュ無効化の管理
  4. 04Redux状態やセレクターの問題の診断

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill redux-toolkit

インストール後、Claude Code で/redux-toolkitを入力して読み込まれたか確認します。

同梱ファイルquick-ref/rtk-query.mdquick-ref/slices.md

詳細

Redux Toolkit のコア知識

詳細な知識:総合的なドキュメントについては、technology を redux-toolkit に指定して mcp__documentation__fetch_docs を使用してください。

スライス

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

interface CounterState {
  value: number;
}

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 } as CounterState,
  reducers: {
    increment: (state) => { state.value += 1; },
    decrement: (state) => { state.value -= 1; },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

ストアの設定

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

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    users: usersReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

非同期サンク

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

export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async (_, { rejectWithValue }) => {
    try {
      const response = await api.getUsers();
      return response.data;
    } catch (err) {
      return rejectWithValue(err.message);
    }
  }
);

// In slice
extraReducers: (builder) => {
  builder
    .addCase(fetchUsers.pending, (state) => { state.loading = true; })
    .addCase(fetchUsers.fulfilled, (state, action) => {
      state.loading = false;
      state.users = action.payload;
    })
    .addCase(fetchUsers.rejected, (state, action) => {
      state.loading = false;
      state.error = action.payload;
    });
}

フック

import { useSelector, useDispatch } from 'react-redux';
import { RootState, AppDispatch } from './store';

export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();

// Usage
const count = useAppSelector((state) => state.counter.value);
const dispatch = useAppDispatch();
dispatch(increment());

このスキルを使用しない場合

シナリオ代わりに使用
シンプルなグローバル状態(ユーザー設定、テーマ)ボイラープレートが少ない zustand
サーバー状態の管理tanstack-query または RTK Query のみ
Vue 3 アプリケーションpinia
小規模なプロジェクトやプロトタイプReact Context + フック、または Zustand
コンポーネントローカルの状態React useState/useReducer

アンチパターン

アンチパターン問題点正しい方法
RTK ではなく素の Redux を使用する大量のボイラープレートが必要で、エラーが発生しやすい常に Redux Toolkit を使用する
Immer を使わずに状態を変更する不変性が損なわれ、バグの原因になるImmer が組み込まれた createSlice を使用する
すべてを Redux に保存する不必要に複雑になる可能な限りコンポーネントの状態をローカルに保つ
型付きフックを使用しない型安全性が失われる型付きの useAppDispatch/useAppSelector をエクスポートする
サンクを使ってコンポーネント内でデータを取得するリクエストのロジックが重複するデータ取得には RTK Query を使用する
createAsyncThunk を使用しない読み込み/エラー処理を手動で行う必要がある非同期アクションには createAsyncThunk を使用する
状態全体を永続化するlocalStorage が肥大化し、ハイドレーションが遅くなる認証や重要なスライスのみを永続化する
サンクにエラー処理がないエラーが通知されずに失敗するサンクで rejectWithValue を使用する
スライス間に循環依存がある保守が難しくなり、バグの原因になるミドルウェアまたは個別のセレクターを使用する
ネストされたデータを正規化しない更新が遅くなり、リデューサーが複雑になる@reduxjs/toolkit/normalizr で正規化する

トラブルシューティング

問題原因解決策
"Cannot read property of undefined"状態のハイドレーションにおける競合状態読み込みチェックを追加するか、skipHydration を使用する
アクションによって再レンダリングが発生しない型付きセレクターを使用していない適切に型付けした useAppSelector を使用する
Redux の "Invariant violation"Immer の外部で状態を変更しているcreateSlice のリデューサー内でのみ変更する
状態が大きいとパフォーマンスが低下するセレクターがメモ化されていないReselect の createSelector を使用する
DevTools にアクションが表示されない本番環境で DevTools が無効になっているdevTools: process.env.NODE_ENV !== 'production' を設定する
RTK Query のキャッシュが無効化されないタグがない、または誤っているprovidesTags と invalidatesTags を追加する
persist でシリアライズエラーが発生する状態にシリアライズできないデータが含まれているserializableCheck.ignoredActions に追加する
サンクのエラーが捕捉されないエラーバウンダリーがないエラーバウンダリーでラップするか、コンポーネントで処理する

本番環境への準備

ストアの設定
// store/index.ts - Production-ready store
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import {
  persistStore,
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER,
} from 'redux-persist';
import storage from 'redux-persist/lib/storage';

const rootReducer = combineReducers({
  auth: authReducer,
  users: usersReducer,
  [api.reducerPath]: api.reducer,
});

const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['auth'], // Only persist auth
  blacklist: [api.reducerPath], // Don't persist API cache
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      },
    }).concat(api.middleware),
  devTools: process.env.NODE_ENV !== 'production',
});

setupListeners(store.dispatch);

export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
RTK Query のベストプラクティス
// services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const baseQuery = fetchBaseQuery({
  baseUrl: '/api',
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).auth.token;
    if (token) {
      headers.set('Authorization', `Bearer ${token}`);
    }
    return headers;
  },
});

const baseQueryWithReauth = async (args, api, extraOptions) => {
  let result = await baseQuery(args, api, extraOptions);

  if (result.error?.status === 401) {
    const refreshResult = await baseQuery('/auth/refresh', api, extraOptions);
    if (refreshResult.data) {
      api.dispatch(setToken(refreshResult.data.token));
      result = await baseQuery(args, api, extraOptions);
    } else {
      api.dispatch(logout());
    }
  }

  return result;
};

export const api = createApi({
  reducerPath: 'api',
  baseQuery: baseQueryWithReauth,
  tagTypes: ['User', 'Post'],
  endpoints: (builder) => ({
    getUsers: builder.query<User[], void>({
      query: () => '/users',
      providesTags: (result) =>
        result
          ? [...result.map(({ id }) => ({ type: 'User' as const, id })), 'User']
          : ['User'],
    }),
    createUser: builder.mutation<User, CreateUserDto>({
      query: (body) => ({
        url: '/users',
        method: 'POST',
        body,
      }),
      invalidatesTags: ['User'],
    }),
  }),
});

export const { useGetUsersQuery, useCreateUserMutation } = api;
Redux のテスト
// test-utils.tsx
import { configureStore } from '@reduxjs/toolkit';
import { render, RenderOptions } from '@testing-library/react';
import { Provider } from 'react-redux';

interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
  preloadedState?: Partial<RootState>;
  store?: ReturnType<typeof configureStore>;
}

export function renderWithProviders(
  ui: React.ReactElement,
  {
    preloadedState = {},
    store = configureStore({
      reducer: rootReducer,
      preloadedState,
    }),
    ...renderOptions
  }: ExtendedRenderOptions = {}
) {
  function Wrapper({ children }: { children: React.ReactNode }) {
    return <Provider store={store}>{children}</Provider>;
  }
  return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}

// Usage
test('displays user list', async () => {
  renderWithProviders(<UserList />, {
    preloadedState: {
      users: { items: [{ id: '1', name: 'John' }], loading: false },
    },
  });

  expect(screen.getByText('John')).toBeInTheDocument();
});
エラー処理
// Centralized error handling middleware
const errorMiddleware: Middleware = () => (next) => (action) => {
  if (isRejectedWithValue(action)) {
    const error = action.payload;

    if (error.status === 401) {
      // Handle unauthorized
      store.dispatch(logout());
    }

    // Log to monitoring service
    logError({
      action: action.type,
      error: error.data?.message || 'Unknown error',
    });
  }

  return next(action);
};
監視メトリクス
メトリクス目標
状態のシリアライズ時間< 50ms
アクションのディスパッチ時間< 16ms
キャッシュヒット率> 80%
テストカバレッジ> 85%
チェックリスト
  • 型付きフック(useAppDispatch、useAppSelector)
  • データ取得に RTK Query を使用
  • キャッシュを自動的に無効化
  • トークン更新の処理
  • 認証状態に Redux Persist を使用
  • 本番環境では DevTools を無効化
  • エラー処理ミドルウェア
  • プリロード済み状態を使ったテストユーティリティ
  • 派生データにメモ化されたセレクターを使用
  • Redux DevTools に機密データを含めない

リファレンスドキュメント

コメント