Redux Pocket Book — Uplatz
50 in-depth cards • Wide layout • Readable examples • 20+ Interview Q&A included
1) What is Redux?
Redux is a predictable state container for JavaScript apps. It centralizes state in a single store, updates state via dispatched actions, and uses pure reducer functions to compute the next state. With Redux Toolkit (RTK), modern Redux is concise and batteries-included.
npm i @reduxjs/toolkit react-redux
2) Three Core Principles
Single source of truth (one store), state is read-only (update via actions), and changes are made with pure reducers. This yields predictable updates and debuggable flows.
// Action
{ type: 'cart/addItem', payload: { id:'p1' } }
3) Redux Toolkit (RTK)
RTK simplifies setup with configureStore
, createSlice
, Immer-powered immutable updates, and good defaults (DevTools, thunk). Prefer RTK over “hand-rolled” Redux.
import { configureStore, createSlice } from '@reduxjs/toolkit'
const cartSlice = createSlice({
name:'cart', initialState:{ items:[] },
reducers:{ addItem:(s,a)=>{ s.items.push(a.payload) } }
})
export const { addItem } = cartSlice.actions
export const store = configureStore({ reducer:{ cart: cartSlice.reducer }})
4) The Data Flow
UI dispatches an action → store runs reducers → new state computed → subscribed components re-render via selectors.
dispatch(addItem({ id:'p1' }))
5) React Binding (react-redux)
Wrap app in <Provider store={store}>
. Components use useSelector
to read state and useDispatch
to dispatch.
import { Provider, useSelector, useDispatch } from 'react-redux'
6) Reducers Must Be Pure
No side effects, no async, no mutations. With RTK, you “mutate” but Immer produces immutable copies safely.
toggle:(s)=>{ s.open = !s.open } // safe via Immer
7) Store Shape & Slices
Split state by domain into slices. RTK merges reducers under keys in configureStore
. Keep state normalized and serializable.
const store = configureStore({ reducer:{ auth:authReducer, cart:cartReducer }})
8) Actions & Action Creators
createSlice
generates action creators and types for you. Keep payloads minimal and well-defined.
dispatch(cartSlice.actions.removeItem('p1'))
9) Selectors
Selectors read state slices. Use memoized selectors (Reselect) for derived data to prevent unnecessary re-renders.
const total = useSelector(s => s.cart.items.length)
10) Q&A — “Why Redux in 2025?”
Answer: For complex apps needing predictable global state, time-travel debugging, middleware, devtools, and ecosystem support. RTK + RTK Query reduce boilerplate dramatically.
11) createSlice
Defines initial state, reducer case reducers, and generates actions. Immer allows mutable-looking code that is actually immutable.
const todos = createSlice({
name:'todos', initialState:[],
reducers:{
add:(s,a)=>{ s.push({ id:Date.now(), title:a.payload, done:false }) },
toggle:(s,a)=>{ const t=s.find(x=>x.id===a.payload); if(t) t.done=!t.done }
}
})
12) configureStore
Creates the store with good defaults: DevTools, thunk, and serializable/immutable checks (can be customized/disabled in prod).
const store = configureStore({ reducer:{ todos: todos.reducer } })
13) createAction & createReducer
Lower-level helpers for custom patterns. Most apps can use createSlice
exclusively.
const increment = createAction('counter/increment')
14) prepare Callbacks
Use prepare
to format payloads (e.g., add IDs, timestamps) before reducer runs.
add: {
reducer:(s,a)=>{ s.push(a.payload) },
prepare:(title)=>({ payload:{ id:nanoid(), title, createdAt:Date.now() } })
}
15) Extra Reducers
Respond to actions defined elsewhere (e.g., RTK Query or thunks) using extraReducers
.
extraReducers:(b)=>{ b.addCase(logout, (s)=>[]) }
16) Middleware Overview
Middleware intercept actions for async, logging, analytics, or side effects. RTK includes thunk by default; you can add custom middleware in middleware:
option.
const mw = store => next => action => { /* ... */ return next(action) }
17) DevTools & Tracing
Redux DevTools enable time travel, action replay, and state diffs. Keep actions serializable; avoid class instances and Dates (serialize). Toggle checks in prod to reduce overhead.
const store = configureStore({ reducer, devTools:true })
18) Normalized State
Store entities by ID (maps + arrays of IDs) using createEntityAdapter
for CRUD helpers and selectors.
const usersAdapter = createEntityAdapter()
const usersSlice = createSlice({ name:'users', initialState:usersAdapter.getInitialState(), reducers:{
upsertMany: usersAdapter.upsertMany
}})
19) Serializability Rules
State/actions should be serializable for DevTools, persistence, and debugging. Use dates as ISO strings, plain objects/arrays. If needed, configure serializableCheck exceptions.
configureStore({ reducer, middleware:(gDM)=>gDM({ serializableCheck:false }) })
20) Q&A — “Why normalized state?”
Answer: Avoids duplication, simplifies updates, and yields O(1) entity access. It also plays nicely with memoized selectors and cache invalidation.
21) Thunks (createAsyncThunk)
Standard way to handle async. Generates pending/fulfilled/rejected action types; handle them in extraReducers. Attach rejectWithValue
for typed errors.
export const fetchUsers = createAsyncThunk('users/fetch', async(_, { rejectWithValue })=>{
try{ const r = await fetch('/api/users').then(r=>r.json()); return r }
catch(e){ return rejectWithValue(e.message) }
})
22) Handling Async States
Keep status
(idle/loading/succeeded/failed) and error
. Reduce UI branching and enable consistent spinners & toasts.
extraReducers:(b)=>{
b.addCase(fetchUsers.pending, (s)=>{ s.status='loading' })
.addCase(fetchUsers.fulfilled, (s,a)=>{ s.status='succeeded'; s.list=a.payload })
.addCase(fetchUsers.rejected, (s,a)=>{ s.status='failed'; s.error=a.payload })
}
23) RTK Query Overview
RTK Query is a data fetching+cache library built on Redux. It auto-generates hooks, handles caching, invalidation, polling, and streaming.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath:'api', baseQuery:fetchBaseQuery({ baseUrl:'/api' }),
endpoints:(b)=>({ getUsers:b.query({ query:()=>'users' }) })
})
export const { useGetUsersQuery } = api
24) RTK Query Setup
Add the API reducer and middleware to the store. Use generated hooks in components.
const store = configureStore({
reducer:{ [api.reducerPath]: api.reducer, other: otherReducer },
middleware:(gDM)=> gDM().concat(api.middleware)
})
25) Caching & Invalidation
Tag-based invalidation lets mutations refresh specific queries. Keep cache logic declarative.
endpoints:(b)=>({
getUser:b.query({ query:(id)=>`users/${id}`, providesTags:(r,id)=>[{type:'User',id}] }),
updateUser:b.mutation({ query:(u)=>({ url:`users/${u.id}`, method:'PUT', body:u }),
invalidatesTags:(r,e,arg)=>[{type:'User', id:arg.id}] })
})
26) Polling & Streaming
RTKQ supports pollingInterval
in hooks and onCacheEntryAdded
for websockets/streams.
const { data } = useGetUsersQuery(undefined, { pollingInterval: 30000 })
27) Optimistic Updates
Use updateQueryData
inside onQueryStarted
for snappy UX; rollback on error.
async onQueryStarted(arg, { dispatch, queryFulfilled }){
const patch = dispatch(api.util.updateQueryData('getUsers', undefined, (draft)=>{ draft.push(arg) }))
try{ await queryFulfilled } catch{ patch.undo() }
}
28) Batching & Performance
React 18 batches updates automatically in most cases. For custom batching of dispatches, you can still use batch
from react-redux
if needed.
import { batch } from 'react-redux'
29) Error Handling Strategy
Normalize errors ({ message, code }) in thunks or RTKQ baseQuery. Surface via toasts/slices. Avoid throwing non-serializable objects into state.
const baseQuery = fetchBaseQuery({ baseUrl:'/api' })
30) Q&A — “Thunks vs RTK Query?”
Answer: Use RTK Query for CRUD APIs, caching, and invalidation. Use thunks for complex workflows that span multiple endpoints, business rules, or non-HTTP side effects.
31) Feature Folder Structure
Organize by feature: each folder holds slice, selectors, components, and tests. Keeps boundaries clear and scaling manageable.
features/
auth/
slice.ts
selectors.ts
Login.tsx
32) Selectors with Reselect
Memoized selectors compute derived data efficiently and prevent useless re-renders.
import { createSelector } from '@reduxjs/toolkit'
const selectItems = (s)=>s.cart.items
export const selectTotal = createSelector([selectItems], (items)=>items.length)
33) Form State: Local vs Redux
Keep transient form input in component state. Put in Redux when multiple pages depend on it (e.g., wizards), or for global persistence/validation workflows.
// Most forms: use local state or React Hook Form
34) Authentication
Store tokens carefully; prefer HTTP-only cookies over localStorage. Keep only necessary user profile data in Redux; avoid secrets in state.
// on logout: dispatch(reset) across slices
35) Entity Adapters
createEntityAdapter
provides adapters with CRUD reducers and memoized selectors per entity set.
const postsAdapter = createEntityAdapter({ sortComparer:(a,b)=>b.date.localeCompare(a.date) })
36) Middleware Examples
Analytics, feature flags, performance logging, auth refresh, and WebSocket forwarding are classic custom middleware use cases.
const analyticsMw = store => next => action => { /* forward to analytics */; return next(action) }
37) SSR & Next.js
Preload data on the server and hydrate the store on the client. Avoid non-serializable values; seal initial state in HTML safely.
const store = makeStore(preloadedState)
38) Code Splitting
Inject reducers at runtime for lazy-loaded routes (e.g., with redux-dynamic-modules
or manual injection patterns). Keep initial bundle small.
// store.injectReducer('feature', featureReducer)
39) Performance Anti-Patterns
Huge connected components, selectors returning new objects each time, overuse of global state, and non-memoized derived data. Fix with memoization, splitting components, entity adapters, and RTKQ.
// Memoize selectors; use shallowEqual if needed
40) Q&A — “Redux vs Zustand/Context?”
Answer: Redux offers a powerful ecosystem, DevTools, middleware, and RTK Query. Zustand or Context may be simpler for small apps; choose Redux for large teams, complex flows, and when you need time-travel debugging & cache-aware data fetching.
41) Testing Reducers
Reducers are pure; test input state + action → output state. No need for DOM.
expect(cartReducer({items:[]}, addItem({id:'1'}))).toEqual({items:[{id:'1'}]})
42) Testing Thunks
Use MSW/fetch-mocks to simulate network and assert dispatched actions. Keep thunks thin.
await store.dispatch(fetchUsers()); expect(store.getState().users.list).toHaveLength(3)
43) Testing Selectors
Provide a sample state and assert the selector output. For memoized selectors, assert referential stability when inputs unchanged.
expect(selectTotal({cart:{items:[1,2]}})).toBe(2)
44) Testing Components
Wrap components with <Provider>
using a test store. Use React Testing Library to assert rendered output and dispatch behavior.
// render(<Provider store={store}><Cart/></Provider>)
45) Redux Persist (Optional)
Persist slices to storage (local/session). Be selective—never persist secrets; handle migrations and versioning.
// redux-persist setup with a whitelist of slice keys
46) Error Boundaries & Redux
Keep UI error boundaries at component level. Store normalized error info, not raw Error objects.
state.error = { message: a.payload, code:'NET_FAIL' }
47) Production Checklist
- RTK store, slices, normalized entities
- Serializability enforced or exceptions documented
- Memoized selectors for heavy derivations
- RTK Query for CRUD APIs & caching
- Strict typing (TS), unit & integration tests
- Performance audits & code splitting
48) Common Pitfalls
Hand-rolling Redux without RTK, putting everything in Redux (vs local state), non-serializable state, giant reducers, and failing to normalize data.
// Prefer RTK + feature folders + adapters
49) Migration Tips (Legacy → RTK)
Wrap existing reducers with combineReducers
inside configureStore
, incrementally replace action creators with slices, and move fetch logic to thunks or RTKQ endpoints.
// Start with store migration, then slice-by-slice refactor
50) Interview Q&A — 20 Practical Questions (Expanded)
1) Why Redux Toolkit? Cuts boilerplate, safe immutable updates via Immer, good defaults, and built-in async/caching with RTK Query.
2) What are reducers? Pure functions (state, action) → new state; no side effects or mutations.
3) When put state in Redux vs component? Shared/cross-page or business-critical state in Redux; transient/local UI state stays local.
4) How to avoid re-renders? Use memoized selectors, split connected components, avoid selectors returning new objects, and normalize state.
5) Thunk vs RTK Query? RTKQ for standard data fetching/caching; thunks for bespoke flows spanning multiple services or non-HTTP side effects.
6) Serializability rules? Keep actions/state serializable for DevTools/persistence; configure exceptions only when necessary.
7) Entity adapter benefits? Fast CRUD reducers, built-in selectors, and consistent normalized state pattern.
8) prepare callback? Preprocess payloads (IDs, timestamps) before reducer logic to keep reducers clean.
9) How does Immer help? Lets you write “mutating” code that produces immutable updates under the hood—safer and faster to write.
10) Why normalized state? Avoid duplicate entities and tricky updates; O(1) lookups and easier memoization.
11) Cache invalidation in RTKQ? Tag types and IDs; mutations invalidate tags so queries refetch automatically.
12) Error handling pattern? Normalize to { message, code }; keep UI boundaries; avoid throwing non-serializable errors into state.
13) SSR with Redux? Preload state server-side, serialize safely, hydrate on client; avoid non-serializable values.
14) Performance debugging? Use React Profiler, Redux DevTools traces, memoized selectors, and split large connected components.
15) Code splitting reducers? Dynamically inject reducers for lazy routes; unregister when leaving if memory matters.
16) Forms & Redux? Prefer local/RHF; use Redux for multi-step shared state, server-synced drafts, or cross-tab persistence.
17) Persisting state? Whitelist slices; add migrations; never persist secrets/PII.
18) Testing thunks? Mock network, assert dispatched actions & final state. Keep thunks thin to simplify tests.
19) Why actions should be minimal? Smaller payloads reduce noise, improve DevTools clarity, and ease refactors.
20) Alternatives to Redux? Zustand, Jotai, Recoil, MobX, or React Query for server cache. Choose per app size, team familiarity, and needs.