redux-best-practices
1
总安装量
1
周安装量
#45662
全站排名
安装命令
npx skills add https://github.com/felipeorlando/redux-best-practices --skill redux-best-practices
Agent 安装分布
mcpjam
1
claude-code
1
kilo
1
windsurf
1
zencoder
1
Skill 文档
Redux Best Practices
Essential Rules (Priority A)
These rules prevent errors. Violating them causes bugs.
1. Never Mutate State
// â WRONG - mutates state
state.todos.push(newTodo)
state.user.name = 'New Name'
// â
CORRECT - RTK with Immer handles this
const todosSlice = createSlice({
reducers: {
todoAdded: (state, action) => {
state.push(action.payload) // Immer makes this safe
}
}
})
2. Reducers Must Be Pure
Forbidden in reducers:
- Async logic (AJAX, timeouts, promises)
Math.random(),Date.now()- External variable modifications
3. Keep State Serializable
Never store: Promises, Symbols, Maps/Sets, Functions, Class instances, DOM nodes
4. One Store Per App
// store.ts - single source of truth
export const store = configureStore({
reducer: { todos: todosReducer, users: usersReducer }
})
Strongly Recommended (Priority B)
Use Redux Toolkit
Always use RTK. It enables DevTools, catches mutations, uses Immer, reduces boilerplate.
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Todo[],
reducers: {
todoAdded: (state, action: PayloadAction<Todo>) => {
state.push(action.payload)
}
}
})
export const { todoAdded } = todosSlice.actions
Feature-Based Structure
src/
âââ app/
â âââ store.ts
â âââ hooks.ts (typed useSelector/useDispatch)
âââ features/
â âââ todos/
â â âââ todosSlice.ts
â â âââ todosSelectors.ts
â â âââ TodoList.tsx
â âââ users/
â âââ usersSlice.ts
â âââ ...
Normalize Complex State
// â Nested - hard to update
{ posts: [{ id: 1, author: { id: 1, name: 'Alice' } }] }
// â
Normalized - easy to update
{
posts: { byId: { '1': { id: '1', authorId: '1' } }, ids: ['1'] },
users: { byId: { '1': { id: '1', name: 'Alice' } }, ids: ['1'] }
}
Model Actions as Events
// â Setter actions
dispatch({ type: 'SET_PIZZA_COUNT', payload: 1 })
dispatch({ type: 'SET_COKE_COUNT', payload: 1 })
// â
Event actions
dispatch({ type: 'food/orderPlaced', payload: { pizza: 1, coke: 1 } })
Treat Reducers as State Machines
interface FetchState {
status: 'idle' | 'loading' | 'succeeded' | 'failed'
data: Data | null
error: string | null
}
// Only allow valid transitions
builder
.addCase(fetchData.pending, (state) => {
if (state.status === 'idle') state.status = 'loading'
})
Use React-Redux Hooks
// â Old connect HOC
export default connect(mapState, mapDispatch)(Component)
// â
Hooks
const todos = useSelector(selectTodos)
const dispatch = useDispatch()
Multiple Granular useSelector Calls
// â Selecting too much - rerenders on any user change
const user = useSelector(state => state.user)
// â
Granular - only rerenders when name changes
const name = useSelector(state => state.user.name)
const email = useSelector(state => state.user.email)
Connect More Components
// â
Parent selects IDs, child selects individual item
const UserList = () => {
const ids = useSelector(selectUserIds)
return ids.map(id => <UserItem key={id} userId={id} />)
}
const UserItem = ({ userId }) => {
const user = useSelector(state => selectUserById(state, userId))
return <div>{user.name}</div>
}
Recommended Patterns (Priority C)
Selector Naming: selectThing
export const selectTodos = (state: RootState) => state.todos
export const selectTodoById = (state: RootState, id: string) =>
state.todos.entities[id]
export const selectCompletedTodos = createSelector(
[selectTodos],
todos => todos.filter(t => t.completed)
)
Action Type Format: domain/eventName
// â
RTK default
'todos/todoAdded'
'users/userLoggedIn'
// â Old SCREAMING_SNAKE_CASE
'ADD_TODO'
Async Logic with Thunks
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async (_, { rejectWithValue }) => {
try {
return await todosAPI.fetchAll()
} catch (err) {
return rejectWithValue(err.message)
}
}
)
// Handle in slice
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, state => { state.status = 'loading' })
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'succeeded'
state.items = action.payload
})
.addCase(fetchTodos.rejected, (state, action) => {
state.status = 'failed'
state.error = action.payload as string
})
}
Use RTK Query for Data Fetching
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getTodos: builder.query<Todo[], void>({
query: () => 'todos'
}),
addTodo: builder.mutation<Todo, Partial<Todo>>({
query: (body) => ({ url: 'todos', method: 'POST', body })
})
})
})
export const { useGetTodosQuery, useAddTodoMutation } = api
Anti-Patterns to Avoid
| Anti-Pattern | Why Bad | Solution |
|---|---|---|
| Form state in Redux | Performance overhead, not global | Local useState |
| UI state in Redux | Modal open/closed isn’t global | Component state |
Blind spread return action.payload |
Loses reducer ownership | Explicit field mapping |
| Sequential dispatches | Multiple renders, invalid states | Single event action |
| Deeply nested state | Complex updates | Normalize |
| Side effects in reducers | Breaks time-travel debug | Use thunks/middleware |
| Selecting entire state slice | Unnecessary rerenders | Granular selectors |
Immutable.js |
Bundle bloat, API infection | Use Immer (built into RTK) |
When NOT to Use Redux
Keep in local component state:
- Form input values (dispatch on submit only)
- UI toggles (modal open, dropdown expanded)
- Animation state
- Hover/focus states
- Data only used by one component
Use Redux for:
- User authentication
- Shopping cart
- Cached API data
- App-wide notifications
- Cross-component shared state
Type-Safe Setup
// app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
References
- See references/slice-patterns.md for complete slice examples with normalized state and async logic