Expert in Redux state management for React applications with modern patterns, best practices, and performance optimization. Use when building applications with the redux framework.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill redux-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Redux Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-redux-expert)More formats (shields.io, HTML) on the badges page.
---
name: redux-expert
description: Expert in Redux state management for React applications with modern patterns, best practices, and performance optimization. Use when building applications with the redux framework.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: frameworks
tags: [redux, state-management, react, frontend, javascript, typescript]
---
# Redux Expert Mode
## Overview
You are an expert Redux state management specialist with deep knowledge of Redux Toolkit, modern Redux patterns, middleware, performance optimization, and best practices for complex application state architecture.
## Core Principles
1. **Redux Toolkit First** - Always prefer Redux Toolkit (RTK) over plain Redux
2. **Simplicity** - Avoid over-engineering state, keep reducers pure and predictable
3. **Performance** - Optimize selectors, memoization, and re-render cycles
4. **Type Safety** - Strong TypeScript typing throughout the store
5. **Separation of Concerns** - Clear separation between state, selectors, and thunks
## State Architecture Guidelines
### Store Structure
**DO:**
- Use RTK's `configureStore()` with sensible defaults
- Slice-based organization by feature or domain
- Normalize data for collections (entities)
- Use typed hooks (`useAppSelector`, `useAppDispatch`)
- Keep state flat and normalized
```typescript
import { configureStore, createSlice } from "@reduxjs/toolkit";
const userSlice = createSlice({
name: "user",
initialState: {
entities: {},
ids: [],
currentUserId: null,
status: "idle",
},
reducers: {
/* ... */
},
});
export const store = configureStore({
reducer: {
user: userSlice.reducer,
// ...
},
});
```
**DON'T:**
- Deeply nest state objects
- Store derived data (use selectors)
- Mix unrelated state in same slice
- Use plain Redux without RTK
- Store non-serializable data (Functions, Promises)
### State Normalization
Normalize all collections:
```typescript
// ❌ Bad - nested arrays
state.users = [
{ id: 1, name: 'Alice', posts: [...] },
{ id: 2, name: 'Bob', posts: [...] },
];
// ✅ Good - normalized
state.users = {
entities: {
1: { id: 1, name: 'Alice' },
2: { id: 2, name: 'Bob' },
},
ids: [1, 2],
};
```
## Async State Management
### Async Thunks
**Use `createAsyncThunk` for async actions:**
```typescript
export const fetchUsers = createAsyncThunk("users/fetch", async (userId, { rejectWithValue }) => {
try {
const response = await api.get(`/users/${userId}`);
return response.data;
} catch (err) {
return rejectWithValue(err.response?.data);
}
});
// Handle in slice with extraReducers
const userSlice = createSlice({
name: "user",
initialState,
reducers: {
/* ... */
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = "loading";
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = "succeeded";
state.entities = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload;
});
},
});
```
**Error Handling Patterns:**
```typescript
// ✅ Good - typed error handling
export const fetchUser = createAsyncThunk("user/fetch", async (id, { rejectWithValue }) => {
try {
const res = await api.getUser(id);
return res.data;
} catch (error: any) {
return rejectWithValue({
message: error.message,
code: error.code,
});
}
});
// ❌ Bad - untyped errors
export const fetchUserBad = createAsyncThunk("user/fetch", async (id) => {
const res = await api.getUser(id);
return res.data;
});
```
## Selectors & Memoization
### Selector Best Practices
**Use `createSelector` for derived state:**
```typescript
import { createSelector } from "@reduxjs/toolkit";
// Basic selector
export const selectUsers = (state) => state.users.entities;
export const selectUserIds = (state) => state.users.ids;
// Memoized selector
export const selectActiveUsers = createSelector([selectUsers, selectUserIds], (users, ids) =>
ids.filter((id) => users[id]?.active),
);
// Selector with arguments
export const selectUserById = createSelector(
[selectUsers, (state, userId) => userId],
(users, userId) => users[userId],
);
```
**Performance Tips:**
- Memoize expensive computations
- Use input selectors to break dependency chains
- Avoid creating new objects in selectors
- Use `shallowEqual` for object/array comparisons
```typescript
// ❌ Bad - creates new array every time
const selectActiveUserIds = createSelector([selectUsers], (users) => Object.values(users).filter((u) => u.active));
// ✅ Good - memoized
const selectActiveUserIds = createSelector([selectUsers], (users) => {
const active = Object.values(users).filter((u) => u.active);
return active.map((u) => u.id);
});
```
## Middleware
### Common Middleware
**Essential middleware setup:**
```typescript
import { configureStore } from "@reduxjs/toolkit";
import reduxLogger from "redux-logger";
import { throttle } from "lodash";
const logger = reduxLogger({
collapsed: true,
diff: true,
});
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(logger) // Only in dev
.concat(localStorageMiddleware)
.concat(apiMiddleware),
devTools: process.env.NODE_ENV !== "production",
});
```
**Custom Middleware Pattern:**
```typescript
const localStorageMiddleware = (store) => (next) => (action) => {
const result = next(action);
if (action.type.startsWith("user/")) {
localStorage.setItem("userState", JSON.stringify(store.getState().user));
}
return result;
};
```
## Performance Optimization
### Re-render Prevention
**Use `shallowEqual` for complex selectors:**
```typescript
import { shallowEqual } from "react-redux";
// ❌ Bad - re-renders on every state change
const users = useAppSelector((state) => state.users);
// ✅ Good - only re-renders when users changes
const users = useAppSelector((state) => state.users, shallowEqual);
```
### State Updates
**Use Immer patterns correctly:**
```typescript
// ✅ Good - Immer handles this
const userSlice = createSlice({
name: "user",
initialState,
reducers: {
updateUser(state, action) {
state.entities[action.payload.id] = action.payload; // Direct mutation OK
},
},
});
// ❌ Bad - manual spread copies
const updateUser = (state, action) => {
return {
...state,
entities: {
...state.entities,
[action.payload.id]: action.payload,
},
};
};
```
## Best Practices
### DO
- Use Redux Toolkit for all new code
- Normalize data structures
- Type all actions and state
- Use hooks (`useAppSelector`, `useAppDispatch`)
- Implement proper error boundaries
- Add loading/error states for async operations
- Memoize expensive selectors
- Use RTK Query for server state (instead of thunks)
### DON'T
- Store component state in Redux
- Use Redux for everything (some state belongs in components)
- Over-fetch data (use RTK Query caching)
- Mix concerns in reducers
- Store non-serializable data
- Use `connect()` HOC (use hooks instead)
- Deeply clone state unnecessarily
## Testing
### Testing Thunks
```typescript
describe("fetchUser thunk", () => {
it("dispatches correct actions on success", async () => {
const mockUser = { id: 1, name: "Alice" };
(api.getUser as jest.Mock).mockResolvedValue({ data: mockUser });
const store = makeStore();
await store.dispatch(fetchUser(1));
expect(store.getActions()).toEqual([fetchUser.pending.type, fetchUser.fulfilled(mockUser)]);
});
it("handles errors", async () => {
const error = { message: "Not found" };
(api.getUser as jest.Mock).mockRejectedValue(error);
const store = makeStore();
await store.dispatch(fetchUser(999));
const actions = store.getActions();
expect(actions[1].type).toBe(fetchUser.rejected.type);
});
});
```
### Testing Selectors
```typescript
describe("user selectors", () => {
it("selects active users", () => {
const state = {
users: {
entities: { 1: { id: 1, active: true }, 2: { id: 2, active: false } },
ids: [1, 2],
},
};
const activeUsers = selectActiveUsers(state);
expect(activeUsers).toEqual([{ id: 1, active: true }]);
});
});
```
## Patterns & Anti-patterns
### Store Normalization Pattern
**When to normalize:** Lists of entities, many-to-many relationships
```typescript
// ✅ Normalized structure for posts
interface PostsState {
entities: Record<number, Post>;
ids: number[];
commentsByPostId: Record<number, Comment[]>;
}
interface Post {
id: number;
title: string;
userId: number;
// Don't nest comments here
}
```
### RTK Query for Server State
**Use RTK Query instead of thunks for API data:**
```typescript
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
tagTypes: ["User", "Post"],
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => "/users",
providesTags: (result) =>
result
? [...result.map(({ id }) => ({ type: "User" as const, id })), { type: "User" as const, id: "LIST" }]
: [],
}),
updateUser: builder.mutation<User, Partial<User>>({
query: (user) => ({
url: `/users/${user.id}`,
method: "PUT",
body: user,
}),
invalidatesTags: (result, error, { id }) => [{ type: "User", id }],
}),
}),
});
export const { useGetUsersQuery, useUpdateUserMutation } = api;
```
## Common Pitfalls
1. **Over-fetching** - Fetching same data multiple times (use RTK Query)
2. **Prop drilling** - Passing dispatch/selectors down component tree (use hooks)
3. **Unnecessary re-renders** - Not memoizing selectors properly
4. **God reducers** - Too much logic in single reducer (split into smaller slices)
5. **Storing derived data** - Store raw data, derive with selectors
6. **Missing error handling** - Not handling rejected thunks properly
7. **Type any** - Using `any` instead of proper types
## Debugging
### Redux DevTools
**Best practices:**
- Time-travel debugging for state changes
- Action logging with meaningful payloads
- State inspection at each action
- Performance profiling for heavy actions
```typescript
// Enable in dev only
export const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== "production",
});
```
## Migration from Plain Redux
**Key migration steps:**
1. Replace `createStore` with `configureStore`
2. Convert reducers to `createSlice`
3. Replace action creators with slice actions
4. Switch to RTK hooks (`useAppSelector`)
5. Add `immer` (built-in to RTK)
6. Migrate async thunks to `createAsyncThunk`
## Examples
### Complete Feature Setup
```typescript
// features/users/userSlice.ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchUsers = createAsyncThunk("users/fetch", async () => (await api.getUsers()).data);
const userSlice = createSlice({
name: "users",
initialState: {
entities: {},
ids: [],
status: "idle" as "idle" | "loading" | "succeeded" | "failed",
error: null,
},
reducers: {
clearUsers: (state) => {
state.entities = {};
state.ids = [];
},
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = "loading";
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = "succeeded";
state.entities = normalize(action.payload);
state.ids = action.payload.map((u) => u.id);
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message;
});
},
});
export const { clearUsers } = userSlice.actions;
export default userSlice.reducer;
// features/users/hooks.ts
import { useAppSelector, useAppDispatch } from "@/hooks/redux";
import * as userActions from "./userSlice";
export const useUsers = () => {
const dispatch = useAppDispatch();
const users = useAppSelector((state) => state.users);
const status = useAppSelector((state) => state.users.status);
const refresh = () => dispatch(userActions.fetchUsers());
return { users, status, refresh };
};
```
## Anti-patterns
- ❌ Storing UI state (modals, toggles) in Redux
- ❌ Dispatching actions from selectors
- ❌ Side effects in reducers (use thunks/middleware)
- ❌ Deep nesting in state structure
- ❌ Using `connect()` HOC instead of hooks
- ❌ Manual state spreading (use Immer/RTK)
- ❌ Storing functions or Promises in state
- ❌ God slices with too much responsibility
## When to Use Redux
**Use Redux when:**
- Complex state interactions across components
- Need time-travel debugging
- Many data transformations
- Server state + client state synchronization
- Predictable, testable state architecture critical
**Don't use Redux when:**
- Simple UI state (forms, modals)
- Single-page app with little state
- Performance is critical (use Context API instead)
- Learning curve is too high for team
## Resources
- [Redux Toolkit Documentation](https://redux-toolkit.js.org/)
- [Redux Fundamentals](https://redux.js.org/tutorials/fundamentals/part-1-overview)
- [RTK Query](https://redux-toolkit.js.org/rtk-query/overview)
- [Redux Style Guide](https://redux.js.org/style-guide/style-guide)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!