Consistent naming in Redux
Action types in Redux follow the pattern domain/ACTION_NAME or DOMAIN_ACTION_NAME. With Redux Toolkit, this simplifies: createSlice automatically generates action creators with names like sliceName/reducerName. Most common mistake: mixing conventions (camelCase vs UPPER_SNAKE_CASE) in the same project.
Recommended structure: one slice per feature, not per data type. Example: cartSlice handles items, totals, discounts (all cart-related), not separate itemsSlice + totalsSlice. This reduces boilerplate and facilitates refactoring.
For async actions with thunks, standard pattern is fetchUsers (thunk) which dispatches fetchUsersPending, fetchUsersFulfilled, fetchUsersRejected. Redux Toolkit generates them automatically with createAsyncThunk. Don't invent variants like fetchUsersStart or fetchUsersSuccess.
Action patterns by use case
Basic CRUD uses 5 minimum actions: fetch (load data), add/create (POST), update/edit (PUT/PATCH), delete/remove (DELETE), and set (replace complete state). UI state needs toggle, open, close, reset for modals, sidebars, dropdowns.
Forms require setFieldValue (individual), setFieldError (validation), resetForm (clear), and submitForm (async). Wizard forms add nextStep, previousStep, goToStep. Auto-save uses saveDraft + debounce.
Realtime with WebSocket: connectSocket (initiate), socketConnected (success), socketError (fail), socketReconnect (retry), disconnectSocket (cleanup). Presence actions: userJoined, userLeft, updatePresence (typing indicators, online status).
Handle side effects and async logic
Redux Toolkit recommends createAsyncThunk for any async logic. Typical example: fetchUsers makes GET, dispatches pending โ fulfilled/rejected automatically. In slice, extraReducers listens to these actions. Avoid async logic directly in reducers (they're pure functions).
Advanced patterns: optimistic updates (update UI before server confirm, rollback if fails), polling (refetch every N seconds with setInterval in thunk), retry logic (retry failed request with exponential backoff). Example: payment thunk retries 3 times before failing.
Error handling: save error messages in state (error: string | null), not just booleans. Rejected actions should include action.error.message for debugging. For multiple simultaneous requests, use Promise.allSettled instead of Promise.all (doesn't break if one fails).
Slice organization in large projects
Recommended folder structure: /features/[feature]/[feature]Slice.ts + [feature]Thunks.ts + [feature]Selectors.ts. Example: /features/auth/authSlice.ts contains login, logout, session. Don't mix all slices in /store/index.ts.
Use configureStore with implicit combineReducers. For normalized state (avoid nested objects), use createEntityAdapter: handles ids, entities, automatic CRUD. Example: usersAdapter.addOne, usersAdapter.updateMany are more efficient than manual spread.
Selectors with createSelector (memoized) prevent unnecessary re-renders. Example: selectActiveUsers = createSelector([selectUsers], users => users.filter(u => u.active)). If users state didn't change, returns same array. Testing: mock store with configureStore({ reducer, preloadedState }), don't use real store in tests.