Actionable rules for building modern, maintainable Redux Toolkit state layers in React apps with TypeScript.
Transform your React state management from a source of complexity into a productivity powerhouse. These battle-tested Cursor rules eliminate Redux boilerplate, enforce type safety, and create maintainable state architecture that scales with your team.
You've been there: Redux stores that grow into unmaintainable monsters. Action creators scattered across dozens of files. Type safety that breaks every time state structure changes. Async logic mixed with reducers. Components that re-render unnecessarily because selectors aren't memoized.
The cost? Hours spent debugging state mutations, hunting down why your API calls aren't working, and explaining to new team members why your Redux setup is "different" from the standard approach.
These rules solve exactly those problems.
These rules transform Redux from a configuration nightmare into a streamlined development experience. You'll build state management that follows Redux Toolkit's modern patterns while maintaining TypeScript strictness and performance optimization.
Key transformations:
createAsyncThunk that manages loading states automaticallycreateEntityAdapter for complex data structures// Before: Hand-written Redux chaos
const FETCH_USER_REQUEST = 'FETCH_USER_REQUEST';
const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS';
const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE';
// After: Redux Toolkit slice
export const fetchUser = createAsyncThunk<User, string>(
'user/fetch',
async (id, { rejectWithValue }) => {
try { return api.getUser(id); }
catch (err) { return rejectWithValue(parseApiError(err)); }
}
);
Reduce Redux Setup Time by 70%
Instead of writing action creators, action types, and reducers separately, createSlice generates everything automatically. What used to take 45 minutes of boilerplate now takes 10 minutes of focused logic.
Eliminate Runtime State Bugs
TypeScript integration with RootState and AppDispatch catches state shape mismatches before they reach production. No more "Cannot read property of undefined" errors from mistyped state access.
Cut Re-render Debugging Time Memoized selectors with Reselect prevent unnecessary component updates. Performance issues that used to require React DevTools investigation are prevented automatically.
Streamline Async Flow Management
createAsyncThunk handles pending/fulfilled/rejected states automatically. No more manually tracking loading states or forgetting to handle error cases.
Old workflow: Create action types, action creators, handle in reducer, write selectors, set up TypeScript types, debug type mismatches.
New workflow: Define interface, create slice with initial state and reducers, export generated actions. Done.
interface UserState {
currentUser: User | null;
isLoading: boolean;
error: string | null;
}
const initialState: UserState = {
currentUser: null,
isLoading: false,
error: null,
};
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action) => {
state.currentUser = action.payload; // Immer handles immutability
},
clearUser: (state) => {
state.currentUser = null;
}
}
});
Instead of manually managing loading states and error handling across multiple actions, async thunks handle the complete flow:
export const fetchUser = createAsyncThunk<User, string>(
'user/fetch',
async (id, { rejectWithValue }) => {
try {
return await api.getUser(id);
} catch (err) {
return rejectWithValue(parseApiError(err));
}
}
);
// Automatically handled in slice:
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => { state.isLoading = true; })
.addCase(fetchUser.fulfilled, (state, action) => {
state.currentUser = action.payload;
state.isLoading = false;
})
.addCase(fetchUser.rejected, (state, action) => {
state.error = action.payload;
state.isLoading = false;
});
}
For normalized data like user lists, posts, or any entity collection, createEntityAdapter eliminates manual normalization logic:
const usersAdapter = createEntityAdapter<User>();
export const usersSlice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState(),
reducers: {
userAdded: usersAdapter.addOne,
userUpdated: usersAdapter.updateOne,
usersLoaded: usersAdapter.setAll,
}
});
// Generated selectors for free
export const {
selectAll: selectAllUsers,
selectById: selectUserById,
selectIds: selectUserIds
} = usersAdapter.getSelectors((state: RootState) => state.users);
Set up your store with TypeScript inference and development tools:
// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
export const store = configureStore({
reducer: {
user: userSlice.reducer,
posts: postsSlice.reducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Typed hooks
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Organize each feature as a self-contained slice:
// features/user/slice.ts
interface UserState {
currentUser: User | null;
isLoading: boolean;
error: string | null;
}
const initialState: UserState = {
currentUser: null,
isLoading: false,
error: null,
};
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action) => {
state.currentUser = action.payload;
},
clearUser: (state) => {
state.currentUser = null;
}
}
});
export const { setUser, clearUser } = userSlice.actions;
// Memoized selectors
export const selectUser = (state: RootState) => state.user.currentUser;
export const selectUserLoading = (state: RootState) => state.user.isLoading;
export default userSlice.reducer;
Use typed hooks for type-safe state access:
// components/UserProfile.tsx
function UserProfile() {
const user = useAppSelector(selectUser);
const isLoading = useAppSelector(selectUserLoading);
const dispatch = useAppDispatch();
const handleLogin = () => {
dispatch(fetchUser('123'));
};
if (isLoading) return <Loading />;
return user ? <UserDetails user={user} /> : <LoginButton onClick={handleLogin} />;
}
Structure your project for maintainability:
src/
app/
store.ts # Store configuration + typed hooks
features/
user/
slice.ts # User state slice
UserProfile.tsx # User components
posts/
slice.ts # Posts state slice
PostList.tsx # Post components
services/
userApi.ts # RTK Query endpoints
Developer Onboarding Acceleration New team members understand Redux patterns immediately because everything follows Redux Toolkit conventions. No more explaining custom Redux patterns or hunting through scattered files.
Bug Reduction
TypeScript integration catches state shape errors at compile time. Immutability bugs are eliminated by Immer. Async state management bugs are prevented by createAsyncThunk's structured approach.
Performance Optimization
Memoized selectors prevent unnecessary re-renders automatically. Normalized state with createEntityAdapter optimizes large dataset operations. Component updates only when actual dependencies change.
Maintenance Simplification Adding new features means creating a new slice, not modifying existing files. Refactoring state shape is caught by TypeScript across your entire application. Testing becomes straightforward with clear action/reducer separation.
Code Review Efficiency Standard patterns mean code reviews focus on business logic, not Redux implementation details. Team members can confidently review any Redux code because it follows consistent patterns.
Ready to transform your Redux development experience? Copy these rules into your Cursor configuration and watch your state management become the most productive part of your React development workflow.
You are an expert in TypeScript, React, Redux Toolkit, RTK Query, Reselect, Redux Thunk, Immer.
Key Principles
- Prefer Redux Toolkit utilities (`configureStore`, `createSlice`, `createAsyncThunk`, `createEntityAdapter`) over hand-written Redux code.
- Keep state minimal, serializable and normalized (use IDs + dictionaries). Derive everything else with selectors.
- Organise logic by domain feature, not UI tree, using one folder per slice.
- Write reducers as pure functions; all side-effects live in thunks or RTK Query endpoints.
- Embrace Immer: mutate draft objects directly inside reducers – Redux Toolkit will handle immutability.
- Co-locate selectors, actions and reducers inside the slice file for discoverability.
- Always enable Redux DevTools and default middleware in the store.
TypeScript Rules
- All Redux code must be TypeScript-strict (`strict: true`).
- Use `RootState` and `AppDispatch` inferred from the store and export typed hooks:
```ts
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```
- Prefer `interface` for public state shapes, `type` for utility unions.
- Slice file layout:
1. `interface XxxState` ➜ initial state
2. `const initialState` ➜ value
3. `export const xxxSlice = createSlice({ … })`
4. `export const { actionA, actionB } = xxxSlice.actions;`
5. Selectors (memoised)
6. `export default xxxSlice.reducer;`
- Name actions in `verbNoun` camelCase (`fetchUser`, `setFilter`). Slice names stay singular (`user`, `cart`).
Error Handling & Validation
- Use `createAsyncThunk` for async flows.
```ts
export const fetchUser = createAsyncThunk<User, string>(
'user/fetch',
async (id, { rejectWithValue }) => {
try { return api.getUser(id); }
catch (err) { return rejectWithValue(parseApiError(err)); }
}
);
```
- In reducers, handle `pending/fulfilled/rejected` separately; store error objects under `error` key.
- Centralise unknown-error logging in custom middleware; re-throw only user-actionable errors.
- Validate external payloads with Zod or io-ts **before** dispatching.
- Never store functions, class instances, Date, or non-serialisable objects in state.
React / Redux Toolkit Integration
- Use functional React components with hooks; avoid `connect` HOC unless legacy.
- Prefer `useAppSelector` + memoised selectors for reading; never read state directly via store inside components.
- Dispatch thunks or RTK Query mutations from UI events, not inside reducers.
- Component folder structure:
`features/` ➜ `featureName/` ➜ `FeatureComponent.tsx`, `slice.ts`, `selectors.ts`, `types.ts`.
RTK Query Rules
- Co-locate API definitions in `services/xxxApi.ts` using `createApi`.
- Set `keepUnusedDataFor` (>=60 s) to reduce refetch churn.
- Use `providesTags` / `invalidatesTags` for cache busting instead of manual `dispatch`.
- Access data with generated hooks (`useGetPostQuery`, `useUpdatePostMutation`). Always check `isLoading`, `error` before using data.
Additional Sections
Testing
- Use `@reduxjs/toolkit`'s `configureStore` in tests to create an in-memory store with the real reducer and middleware.
- Unit-test reducers with plain objects:
```ts
expect(userReducer(undefined, action)).toEqual(expectedState);
```
- Test async thunks with `jest.fn()` mocks and `await store.dispatch(thunk())`.
Performance
- Normalise large collections with `createEntityAdapter`.
- Memoise derived data using Reselect’s `createSelector`; avoid anonymous selectors in components.
- Use `shallowEqual` in `useAppSelector` when selecting multiple primitive keys.
- Avoid unnecessary re-renders by splitting state into multiple slices instead of one giant slice.
Security
- Strip sensitive data (tokens, passwords) before putting into Redux; store them in secure HTTP-only cookies or memory.
- Never log whole state to analytics; use selector to pick minimal safe subset.
Folder Convention (Top Level)
```
src/
app/
store.ts # configureStore() + typed hooks
rootReducer.ts # combines slice reducers if needed
features/
user/
slice.ts
selectors.ts
UserPage.tsx
posts/
…
services/
userApi.ts # RTK Query endpoints
utils/
types.ts
```
Happy coding: keep slices small, selectors memoised, state serialisable, and side-effects contained!