Redux Cursor Rules: State Management with Redux Toolkit

Cursor rules for Redux covering Redux Toolkit configureStore, createSlice patterns, Immer immutability, memoized selectors with createSelector, async thunks, RTK Query data fetching, typed hooks, and normalized state shapes.

June 11, 2026by PromptGenius Team
reduxcursor-rulesstate-managementreactredux-toolkittypescript
Redux Cursor Rules: State Management with Redux Toolkit

Overview

Redux is the most widely used state management library in the React ecosystem, with Redux Toolkit (RTK) as the official, modern approach. These cursor rules enforce configureStore setup, createSlice patterns with Immer-powered immutable updates, memoized selectors with createSelector, async logic with createAsyncThunk, RTK Query for server state, typed hooks, normalized state shapes with createEntityAdapter, and middleware best practices so AI assistants generate scalable, maintainable Redux code.

Note:

Enforces Redux Toolkit patterns (not legacy Redux), createSlice with Immer for immutable updates, createSelector for memoized derived state, createAsyncThunk with pending/fulfilled/rejected handling, RTK Query createApi for data fetching, typed useAppDispatch/useAppSelector hooks, createEntityAdapter for normalized collections, and one slice per feature domain.

Rules Configuration

---
description: Enforces Redux Toolkit best practices including configureStore, createSlice, memoized selectors, createAsyncThunk, RTK Query, typed hooks, normalized state, and middleware patterns.
globs: **/store/**/*.ts,**/store/**/*.tsx,**/*.slice.ts,**/*Slice.ts,src/store.ts,src/app/store.ts,**/slices/**/*.ts
---
# Redux Toolkit Best Practices

You are an expert in Redux Toolkit, React state management, and TypeScript.
You understand Redux Toolkit, RTK Query, Immer, memoized selectors, and scalable store architecture.

### Store Setup (configureStore)
- Use `configureStore` from `@reduxjs/toolkit` — never `createStore` from core Redux
- Combine slices with the `reducer` object syntax: `reducer: { users: usersReducer, posts: postsReducer }`
- Middleware includes `redux-thunk` by default — add `.concat(apiMiddleware)` for RTK Query
- Add DevTools with `devTools: process.env.NODE_ENV !== 'production'`
- NEVER modify middleware by slicing the defaults array — use `.concat()` and `.prepend()`
- Export `RootState` type: `export type RootState = ReturnType<typeof store.getState>`
- Export `AppDispatch` type: `export type AppDispatch = typeof store.dispatch`
- Create the store in a single file: `src/app/store.ts` or `src/store.ts`

### Slices (createSlice)
- Use `createSlice` — never `createReducer` directly unless composing reducers
- Every slice needs: `name`, `initialState`, `reducers`
- Organize one slice per feature domain: `usersSlice`, `postsSlice`, `authSlice`
- NEVER spread state manually — let Immer handle immutability inside reducers
- Mutate state directly inside `reducers` — Immer makes it safe: `state.users.push(action.payload)`
- Use `prepare` callback for complex action creators inside reducers
- Export actions: `export const { addUser, removeUser, updateUser } = usersSlice.actions`
- Export the reducer as default: `export default usersSlice.reducer`
- Use `extraReducers` with builder syntax for handling external actions and async thunks
- Handle async thunk lifecycle: `builder.addCase(fetchUsers.pending, (state) => { state.loading = true })`

### Selectors (createSelector)
- Use `createSelector` from `@reduxjs/toolkit` for memoized, derived selectors
- Define base selectors in the slice file: `const selectUsersState = (state: RootState) => state.users`
- Compose selectors: `createSelector([selectUsersState, selectFilter], (users, filter) => ...)`
- Keep selectors pure — no side effects, no API calls
- Use selectors inside components with `useAppSelector`: `const users = useAppSelector(selectFilteredUsers)`
- NEVER use `useAppSelector` with inline arrow functions — extract to named selectors for memoization
- Use `createDraftSafeSelector` for Immer-aware selectors when operating on draft state
- Name selectors with `select` prefix: `selectUserById`, `selectActiveUsers`

### Async Logic (createAsyncThunk)
- Use `createAsyncThunk` for API calls: `createAsyncThunk<ReturnType, ArgType>('users/fetchById', async (id, thunkAPI) => { ... })`
- NEVER write thunk functions manually — use `createAsyncThunk`
- Handle all three states in extraReducers: `pending`, `fulfilled`, `rejected`
- Use `thunkAPI.rejectWithValue(error.response.data)` for custom error payloads
- Type the thunk with generics: `<ReturnType, ArgType, { rejectValue: ErrorType }>`
- Export thunk actions, not the thunk creator: `export const fetchUsers = createAsyncThunk(...)`
- Handle loading and error states in the slice: `loading: boolean`, `error: string | null`
- NEVER mutate state in async thunks — only in reducers/extraReducers

### RTK Query (Data Fetching)
- Use `createApi` for server state: `createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: '/api' }), endpoints: (builder) => ({ ... }) })`
- Define endpoints with `builder.query()` for GET and `builder.mutation()` for POST/PUT/DELETE
- Use `tagTypes` and `providesTags`/`invalidatesTags` for automatic cache invalidation
- Export auto-generated hooks: `export const { useGetUsersQuery, useAddUserMutation } = apiSlice`
- Use hooks in components: `const { data, isLoading, error } = useGetUsersQuery()`
- NEVER manually refetch after mutations — use tags for automatic cache invalidation
- Set `keepUnusedDataFor` for aggressive caching: `keepUnusedDataFor: 300` (5 minutes)
- Split large APIs using `injectEndpoints`: `apiSlice.injectEndpoints({ endpoints: ... })`

### Typed Hooks
- Create typed hooks in store file:
  ```typescript
  import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
  export const useAppDispatch: () => AppDispatch = useDispatch
  export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
  • NEVER use raw useDispatch or useSelector — always use the typed versions
  • Import typed hooks from the store file, not from react-redux directly

Normalized State (createEntityAdapter)

  • Use createEntityAdapter for collections: const usersAdapter = createEntityAdapter<User>({ sortComparer: (a, b) => a.name.localeCompare(b.name) })
  • Adapter provides: addOne, addMany, setAll, removeOne, updateOne, upsertOne
  • Initial state: usersAdapter.getInitialState({ loading: false, error: null })
  • Selectors: usersAdapter.getSelectors((state) => state.users)
  • Entity shape: { ids: string[], entities: Record<string, T> } — normalized by default
  • Use selectId option for custom ID fields: selectId: (user) => user.email

State Shape Best Practices

  • Keep state minimal — only store what can't be derived
  • Prefer storing IDs over full objects for relationships: commentIds: string[] not comments: Comment[]
  • NEVER store computed/derived values in state — use selectors
  • NEVER store form state in Redux — use local component state unless multi-step
  • NEVER store router state in Redux — use the router library's built-in state
  • NEVER put non-serializable values in Redux state: no Promises, functions, class instances
  • Structure slices by domain, not by page or component
  • Extract reusable state patterns into shared slices or adapters

## Installation

Create `redux.mdc` in your project's `.cursor/rules/` directory and paste the configuration above. Cursor and Windsurf both read `.cursor/rules/` — Copilot users place it in `.github/copilot-instructions.md` instead.

```bash
npm install @reduxjs/toolkit react-redux

# src/app/store.ts — Create the store
# src/features/users/usersSlice.ts — Create your first slice
# Then wrap your app with <Provider store={store}>

Examples

// src/app/store.ts — Store setup with typed hooks
import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import usersReducer from "@/features/users/usersSlice";
import { apiSlice } from "@/services/api";

export const store = configureStore({
  reducer: {
    users: usersReducer,
    [apiSlice.reducerPath]: apiSlice.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(apiSlice.middleware),
});

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

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// src/features/users/usersSlice.ts — Slice with async thunk and Immer
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "@/app/store";
import { fetchUserById } from "@/services/api";

interface User {
  id: string;
  name: string;
  email: string;
  role: "admin" | "user";
}

interface UsersState {
  items: Record<string, User>;
  loading: boolean;
  error: string | null;
}

const initialState: UsersState = {
  items: {},
  loading: false,
  error: null,
};

export const fetchUser = createAsyncThunk<User, string, { rejectValue: string }>(
  "users/fetchById",
  async (userId, { rejectWithValue }) => {
    try {
      return await fetchUserById(userId);
    } catch (err: any) {
      return rejectWithValue(err.message);
    }
  }
);

const usersSlice = createSlice({
  name: "users",
  initialState,
  reducers: {
    addUser(state, action: PayloadAction<User>) {
      state.items[action.payload.id] = action.payload;
    },
    removeUser(state, action: PayloadAction<string>) {
      delete state.items[action.payload];
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false;
        state.items[action.payload.id] = action.payload;
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload ?? "Failed to fetch user";
      });
  },
});

export const { addUser, removeUser } = usersSlice.actions;

// Memoized selectors
const selectUsersState = (state: RootState) => state.users;
export const selectAllUsers = (state: RootState) => Object.values(selectUsersState(state).items);
export const selectUserById = (id: string) => (state: RootState) => selectUsersState(state).items[id];
export const selectUsersLoading = (state: RootState) => selectUsersState(state).loading;

export default usersSlice.reducer;