useMutation({
mutationFn: (id: string) => api.toggleFavourite(id),
onMutate: async (id) => {
await qc.cancelQueries({ queryKey: productKeys.lists() });
const previous = qc.getQueryData<Product[]>(productKeys.list(filters));
qc.setQueryData<Product[]>(productKeys.list(filters), (old) =>
old?.map((p) => (p.id === id ? { ...p, favourite: !p.favourite } : p)),
);
return { previous };
},
onError: (_err, _id, ctx) => qc.setQueryData(productKeys.list(filters), ctx?.previous),
onSettled: () => qc.invalidateQueries({ queryKey: productKeys.lists() }),
});
State Management & Data Fetching
draftedLocal vs global vs server state, Redux Toolkit & RTK Query, TanStack Query (cache, staleTime, invalidation), Zustand/Context, optimistic updates, real-time data vào cache, SSR store per request.
0/45 rated · mastery 0%
Bài học
10 bài · ~135 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~17 phút· 7 câuverify
Phân loại state: server, client, URL, form và derived
Vì sao câu hỏi đầu tiên là 'state này thuộc loại nào', cách chọn nơi đặt từng loại state, và khi nào không cần global store.
- 02~14 phút· 7 câuverify
Redux core và Redux Toolkit
Store, action, reducer, dispatch, Immer và structural sharing, configureStore dev checks, serializability, createEntityAdapter và cấu trúc feature cho codebase lớn.
- 03~13 phút· 5 câuverify
Selectors và re-render: Redux, Zustand, Context
useSelector so sánh bằng ===, createSelector và weakMapMemoize, Zustand useShallow và lỗi Maximum update depth ở v5, tách Context state/actions, và cách đo re-render.
- 04~12 phút· 4 câuverify
Side effects trong Redux: thunk, listener, saga
Middleware là nơi side effect sống; createAsyncThunk với signal, condition, requestId; listener middleware cho debounce và cancel; saga; và kế hoạch migrate một app lưu mọi response vào Redux.
- 05~15 phút· 6 câuverify
TanStack Query: query key, staleTime, gcTime và trạng thái
Vòng đời một query trong QueryCache, dedupe, fresh vs stale, gcTime, isPending/isFetching/isLoading, placeholderData khi phân trang, retry và refetch mặc định, và cách debug dashboard gọi API trùng.
- 06~13 phút· 5 câuverify
Mutation và invalidation: list, detail và infinite query
useMutation và thứ tự callback, invalidateQueries vs refetchQueries vs setQueryData vs resetQueries vs removeQueries, giữ list và detail nhất quán, infinite query với cursor và maxPages, và cách đi qua các tầng cache khi thấy giá cũ.
- 07~11 phút· 4 câuverify
RTK Query: API slice, tags và cache lifecycle
createApi và endpoint definition, subscription và keepUnusedDataFor, providesTags/invalidatesTags với pattern LIST + id, onCacheEntryAdded cho streaming update, và chọn giữa RTK Query và TanStack Query.
- 08~13 phút· 4 câuverify
Optimistic updates và rollback
onMutate, cancelQueries, snapshot, rollback và reconcile; race khi bấm liên tục và mutation scope; RTK Query updateQueryData + patch.undo; React 19 useOptimistic và Server Actions; xung đột 409/412 khi hai người cùng sửa.
- 09~13 phút· 5 câuverify
Real-time, đồng bộ đa tab, persist và offline
Đưa WebSocket/Socket.IO event vào query cache (patch hay invalidate), chống event đến sai thứ tự bằng version, buffer để không làm ngập React, đồng bộ tab bằng BroadcastChannel, persist có version/migrate, và outbox cho app offline.
- 10~13 phút· 6 câuverify
SSR, logout và cache multi-tenant
Vì sao Redux store và QueryClient phải tạo per request trên server, prefetch + dehydrate + HydrationBoundary trong Next.js App Router, mọi nơi state có thể rò sau logout, và thiết kế cache cho app admin multi-tenant.
Tóm tắt & cheat sheet của track
TL;DR
- Câu hỏi đầu tiên không phải "Redux hay Zustand" mà là state này thuộc loại nào: server state (dữ liệu của backend, cache có thể cũ), client/UI state (chỉ tồn tại ở browser), URL state, form draft.
- Server state cần: cache, dedupe, stale-while-revalidate, refetch, invalidation, retry, pagination → dùng TanStack Query hoặc RTK Query (hoặc RSC ở Next.js). Copy response vào global store thủ công là nguồn bug số 1.
- Client state toàn cục (nhiều màn hình cùng ghi/đọc) → Redux Toolkit / Zustand / Jotai; ít đổi → Context.
- Optimistic update = cập nhật cache trước, rollback khi lỗi, luôn reconcile với server (invalidate/refetch) sau cùng.
- Selector + reference stability quyết định hiệu năng: selector trả object mới mỗi lần = re-render mỗi lần.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Server state | Bản sao có thể cũ của dữ liệu thuộc backend | danh sách order, profile |
| Client state | Dữ liệu chỉ có ở client | modal mở, theme, bước wizard, cart offline |
| Query key | Định danh cache entry (TanStack Query) | ['orders', { status, page }] |
staleTime | Bao lâu dữ liệu được coi là fresh (không refetch khi mount/focus) | default 0 → luôn stale |
gcTime | Entry không còn observer bị xoá khỏi cache sau bao lâu | default 5 phút (v5, trước là cacheTime) |
| Invalidation | Đánh dấu stale + refetch query đang active | invalidateQueries({ queryKey: ['orders'] }) |
| Tags (RTK Query) | Query providesTags, mutation invalidatesTags | ['Order', { type: 'Order', id }] |
| Selector | Hàm đọc một phần store; memoized khi derive | createSelector([selectItems], items => total(items)) |
| Normalization | Lưu entity theo id, list chỉ giữ id | createEntityAdapter() → { ids, entities } |
| Optimistic update | Cập nhật UI trước khi server xác nhận | like/unlike, kéo thả sắp xếp |
Cơ chế hoạt động
TanStack Query — vòng đời một query
Optimistic update có rollback (TanStack Query)
Redux Toolkit data flow
Trade-offs
| Tool | Mạnh | Yếu | Hợp khi |
|---|---|---|---|
Context + useReducer | Không thêm dependency | Không selector → consumer re-render hết | Theme, auth, locale, DI |
| Redux Toolkit | Kiến trúc rõ, DevTools, middleware, team lớn | Boilerplate hơn, học nhiều khái niệm | App lớn, nhiều người, logic client phức tạp |
| RTK Query | Cache server state tích hợp Redux, tags | Gắn với Redux; normalized cache không có sẵn | Đã dùng Redux |
| TanStack Query | Server state mạnh nhất, devtools, infinite, SSR hydration | Chỉ lo server state | Hầu hết app có API |
| Zustand | Nhỏ, selector, ngoài React dùng được | Ít convention, dễ thành "bãi rác" | Client state vừa phải |
| Jotai / atoms | State dạng nguyên tử, derive tự nhiên | Graph atom khó theo dõi khi lớn | UI state phân mảnh, editor |
| URL (search params) | Share link, back/forward | Chỉ serializable, giới hạn độ dài | Filter, sort, page, tab |
Pitfalls
- Lưu response API vào Redux và React Query → hai nguồn sự thật lệch nhau.
staleTime: 0mặc định + nhiều component cùng key → refetch khi mỗi component mount/khi focus — tưởng là bug "gọi API liên tục".- Query key thiếu biến (
['orders']nhưng queryFn dùngstatus) → cache trả nhầm dữ liệu. - Selector trả object/array mới (
state => ({ a: state.a }),.filter) → re-render mỗi action. - Tạo Redux store/QueryClient ở module scope trong app SSR → dữ liệu user A lộ sang user B.
- Optimistic update quên
cancelQueries→ refetch đang bay ghi đè giá trị optimistic. - Thunk/effect không huỷ khi rời màn hình → set state vào store của màn hình khác.
- Lưu dữ liệu non-serializable (Date, class, Promise) vào Redux → DevTools/persist hỏng.
Cheat sheet
- TanStack Query v5:
staleTime0,gcTime5 phút,retry3 (client), refetch khi mount/focus/reconnect nếu stale.isPending= chưa có data;isFetching= đang fetch (kể cả nền);isLoading=isPending && isFetching. invalidateQueries(stale + refetch active) vsrefetchQueries(fetch ngay) vssetQueryData(ghi tay) vsremoveQueries.- Query key: array, serializable, chứa mọi biến queryFn dùng; dùng key factory.
- RTK:
configureStore(thunk + dev checks),createSlice(Immer),createAsyncThunk,createEntityAdapter,createListenerMiddleware,createSelector. - RTK Query:
providesTags/invalidatesTags,keepUnusedDataFor(default 60s), optimistic quaonQueryStarted+updateQueryData+patch.undo(). - Zustand: luôn select slice; nhiều field →
useShallow. - SSR/Next.js: store & QueryClient per request; Server Components không dùng được store client.
References
- Redux — Redux Essentials tutorial ↗
- Redux — Style Guide ↗
- Redux Toolkit — RTK Query overview ↗
- Redux Toolkit — Optimistic updates (RTK Query) ↗
- Redux Toolkit — Setup with Next.js ↗
- TanStack Query — Important defaults ↗
- TanStack Query — Query keys ↗
- TanStack Query — Optimistic updates ↗
- TanStack Query — Migrating to v5 ↗
- TanStack Query — Advanced SSR ↗
- Zustand — docs ↗
- react.dev — Choosing the state structure ↗
Interview Questions
Showing 45 / 45
getOrders: build.query<Order[], void>({
query: () => '/orders',
providesTags: (result = []) => [
{ type: 'Order', id: 'LIST' },
...result.map(({ id }) => ({ type: 'Order' as const, id })),
],
}),
updateOrder: build.mutation<Order, Partial<Order> & { id: string }>({
query: ({ id, ...patch }) => ({ url: `/orders/${id}`, method: 'PATCH', body: patch }),
invalidatesTags: (_r, _e, { id }) => [{ type: 'Order', id }],
}),
export function useOrders(filters: { status: string; page: number }) {
const { tenantId } = useTenant();
return useQuery({
queryKey: ['orders', filters.page],
queryFn: () => api.get('/orders', { params: { ...filters, tenantId } }),
staleTime: 5 * 60_000,
});
}
const useCart = create<CartState>()((set) => ({
items: [],
coupon: null,
add: (item) => set((s) => ({ items: [...s.items, item] })),
}));
function CartBadge() {
const { count, hasCoupon } = useCart((s) => ({
count: s.items.length,
hasCoupon: s.coupon !== null,
}));
return <span>{count}{hasCoupon && '*'}</span>;
}
useMutation({
mutationFn: renameProject,
onMutate: ({ id, name }) => {
queryClient.setQueryData(['project', id], (p: Project) => ({ ...p, name }));
},
onSettled: (_d, _e, { id }) => queryClient.invalidateQueries({ queryKey: ['project', id] }),
});
useEffect(() => {
const promise = dispatch(fetchReport(filters));
return () => promise.abort();
}, [dispatch, filters]);