Redux: Predictable State Container (RTK + RTK Query)
Redux is most valuable when you need a shared state architecture that remains understandable as the app grows. Modern Redux means Redux Toolkit (RTK) everywhere, and often RTK Query for server data.
Interviewers Care More About
- choosing Redux for the right reasons
- keeping state normalized
- selector-driven performance
- predictable async flows
Quick Decision Guide
Use Redux when: many screens share state, complex workflows, strong debugging + team consistency.
Use RTK Query when: you’re managing server data (fetching, caching, invalidation).
### Quick Setup (Redux Toolkit)
1) Create slice
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
addBy: (state, action) => { state.value += action.payload }
}
})
export const { increment, addBy } = counterSlice.actions
export default counterSlice.reducer2) Configure store
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
export const store = configureStore({
reducer: { counter: counterReducer }
})3) Use in React
import { Provider, useDispatch, useSelector } from 'react-redux'
const count = useSelector((s) => s.counter.value)
const dispatch = useDispatch()
dispatch(increment())### RTK Query (recommended for server data)
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post'],
endpoints: (build) => ({
getPosts: build.query({
query: () => '/posts',
providesTags: (result = []) => [
'Post',
...result.map((p) => ({ type: 'Post', id: p.id }))
]
}),
updatePost: build.mutation({
query: (body) => ({ url: `/posts/${body.id}`, method: 'PUT', body }),
invalidatesTags: (r, e, body) => [{ type: 'Post', id: body.id }]
})
})
})Big idea: Redux Toolkit for app state; RTK Query for server cache; selectors for performance.
Mental Model & Architecture
Redux Mental Model
Redux is a single state tree updated by dispatching actions.
Flow: UI → dispatch(action) → reducer computes nextState → subscribers re-render.
What Redux Gives You
Common Mistake
Using Redux for everything (especially server data) leads to too much boilerplate.
Rule:
Redux Toolkit (RTK) Patterns
Slices (state + reducers + actions)
RTK uses Immer, so reducers can "mutate" draft state safely.
import { createSlice } from '@reduxjs/toolkit'
const uiSlice = createSlice({
name: 'ui',
initialState: { modalOpen: false, toast: null },
reducers: {
openModal: (s) => { s.modalOpen = true },
closeModal: (s) => { s.modalOpen = false },
showToast: (s, a) => { s.toast = a.payload }
}
})Async (Two recommended options)
1) RTK Query for server data (preferred)
2) createAsyncThunk for workflows not tied to API caching
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
export const fetchUser = createAsyncThunk('user/fetch', async (id) => {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error('Request failed')
return res.json()
})
const userSlice = createSlice({
name: 'user',
initialState: { entity: null, status: 'idle', error: null },
reducers: {},
extraReducers: (b) => {
b.addCase(fetchUser.pending, (s) => { s.status = 'loading'; s.error = null })
.addCase(fetchUser.fulfilled, (s, a) => { s.status = 'succeeded'; s.entity = a.payload })
.addCase(fetchUser.rejected, (s, a) => { s.status = 'failed'; s.error = a.error.message })
}
})Performance: Normalization + Selectors (Most Important)
Normalize State Shape
Avoid deep nesting. Store entities by id.
// ✅ normalized
{
users: { byId: { '1': { id:'1', name:'A' } }, allIds: ['1'] },
posts: { byId: { '10': { id:'10', userId:'1' } }, allIds: ['10'] }
}Select minimal state in components
Anti-pattern: selecting big objects causes rerenders.
// ❌ rerenders more than needed
const userState = useSelector((s) => s.user)
// ✅ minimal selection
const userName = useSelector((s) => s.user.entity?.name)Derived data: memoize with reselect
import { createSelector } from '@reduxjs/toolkit'
const selectPostsById = (s) => s.posts.byId
const selectFilter = (s) => s.ui.filter
export const selectFilteredPosts = createSelector(
[selectPostsById, selectFilter],
(byId, filter) => Object.values(byId).filter((p) => p.category === filter)
)Rule of thumb
RTK Query: Server State Done Right
Why RTK Query
Pattern: tags for invalidation
providesTags for queriesinvalidatesTags for mutationsThis keeps UI consistent without manual refetch logic.
Next.js Notes (App Router)
Next.js App Router Notes
Tip: if you need per-request server state, don't try to "SSR" a global Redux store blindly; design explicit hydration boundaries.