Redux Pocket Book

Redux Pocket Book — Uplatz

50 in-depth cards • Wide layout • Readable examples • 20+ Interview Q&A included

Section 1 — Foundations

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.

Section 2 — Redux Toolkit Deep Dive

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.

Section 3 — Async Logic, RTK Query & Side Effects

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.

Section 4 — Architecture, Patterns & Integrations

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.

Section 5 — Testing, Checklist & Interview Q&A

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.