Skip to content
DevRecall
← Roadmap
10

State Management & Data Fetching

drafted

Local 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.
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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ũ.

  7. 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.

  8. 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.

  9. 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. 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ệmMột câuVí dụ ngắn
Server stateBản sao có thể cũ của dữ liệu thuộc backenddanh sách order, profile
Client stateDữ liệu chỉ có ở clientmodal mở, theme, bước wizard, cart offline
Query keyĐịnh danh cache entry (TanStack Query)['orders', { status, page }]
staleTimeBao lâu dữ liệu được coi là fresh (không refetch khi mount/focus)default 0 → luôn stale
gcTimeEntry không còn observer bị xoá khỏi cache sau bao lâudefault 5 phút (v5, trước là cacheTime)
InvalidationĐánh dấu stale + refetch query đang activeinvalidateQueries({ queryKey: ['orders'] })
Tags (RTK Query)Query providesTags, mutation invalidatesTags['Order', { type: 'Order', id }]
SelectorHàm đọc một phần store; memoized khi derivecreateSelector([selectItems], items => total(items))
NormalizationLưu entity theo id, list chỉ giữ idcreateEntityAdapter() → { ids, entities }
Optimistic updateCập nhật UI trước khi server xác nhậnlike/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

ToolMạnhYếuHợp khi
Context + useReducerKhông thêm dependencyKhông selector → consumer re-render hếtTheme, auth, locale, DI
Redux ToolkitKiến trúc rõ, DevTools, middleware, team lớnBoilerplate hơn, học nhiều khái niệmApp lớn, nhiều người, logic client phức tạp
RTK QueryCache server state tích hợp Redux, tagsGắn với Redux; normalized cache không có sẵnĐã dùng Redux
TanStack QueryServer state mạnh nhất, devtools, infinite, SSR hydrationChỉ lo server stateHầu hết app có API
ZustandNhỏ, selector, ngoài React dùng đượcÍt convention, dễ thành "bãi rác"Client state vừa phải
Jotai / atomsState dạng nguyên tử, derive tự nhiênGraph atom khó theo dõi khi lớnUI state phân mảnh, editor
URL (search params)Share link, back/forwardChỉ serializable, giới hạn độ dàiFilter, sort, page, tab

Pitfalls

  • Lưu response API vào Redux và React Query → hai nguồn sự thật lệch nhau.
  • staleTime: 0 mặ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ùng status) → 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: staleTime 0, gcTime 5 phút, retry 3 (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) vs refetchQueries (fetch ngay) vs setQueryData (ghi tay) vs removeQueries.
  • 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 qua onQueryStarted + 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.

Personal notes (Notion)

Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.

Interview Questions

Showing 45 / 45

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() }),
});
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]);