Back to skills
SKILL.md
Redux Expert
ASecurityExpert in Redux state management for React applications with modern patterns, best practices, and performance optimization. Use when building applications with the redux framework.
- 9 stars
- 0 votes
- 0 copies
- 6 views
- Added September 8, 2026
Works with
Security analysis
100/100npx -y skills add anubhavg-icpl/vibe --skill redux-expert --agent claude-codeAre you the author of Redux Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-redux-expert)---
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)
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.