Module 06 — State Management

Phải có &9201; 10-12 giờ &128203; Prerequisites: Module 01-04

&127919; Mục tiêu học tập

&128214; Hướng dẫn học

1 Đọc Redux Toolkit docsredux-toolkit.js.org/tutorials/quick-start
Focus: Quick Start, createSlice, configureStore, createAsyncThunk.
Thời gian: ~2 giờ. Chạy từng example, đọc kỹ phần Immer integration.
2 Build Todo App với Redux Toolkit
Tạo full CRUD todo app: add, toggle, delete, filter (all/active/completed).
Thêm async: fetch todos từ JSONPlaceholder API với createAsyncThunk.
Thời gian: ~2-3 giờ
3 Đọc Zustand docsdocs.pmnd.rs/zustand
Focus: basic usage, selectors, persist middleware, devtools middleware.
Thời gian: ~1 giờ
4 Refactor Todo App sang Zustand
Convert cùng Todo app từ Redux sang Zustand. So sánh boilerplate, DX, performance.
Thời gian: ~1-2 giờ
5 Đọc TanStack Query docstanstack.com/query
Focus: useQuery, useMutation, useInfiniteQuery, caching, staleTime vs gcTime.
Thời gian: ~2 giờ
6 So sánh Developer Experience
Viết 1 bài blog ngắn hoặc notes so sánh Redux vs Zustand vs React Query.
Focus: boilerplate, learning curve, performance, use cases.
Thời gian: ~1 giờ

&9889; Ôn nhanh

🧭 Vòng lặp học module này

&128204; Decision tree chọn state tool

  1. State chỉ dùng trong 1 component? Dùng useState hoặc useReducer.
  2. State là data từ API? Dùng TanStack Query trước, chỉ đưa vào store nếu có lý do rõ.
  3. State cần đọc/ghi ở nhiều screen nhưng logic đơn giản? Dùng Zustand hoặc Context split nhỏ.
  4. State nhiều action, audit/debug quan trọng, team quen Redux? Dùng Redux Toolkit.
  5. Cần undo/redo, workflow phức tạp, nhiều derived data? Cân nhắc reducer/state machine + selector memoized.

&128218; Lý thuyết chi tiết

1. Client State vs Server State — Mental Model

Tại sao phân biệt này quan trọng? Đây là sai lầm phổ biến nhất của React developer: dùng Redux để cache API data. Hai loại state này có bản chất hoàn toàn khác nhau, cần tools khác nhau.

Tiêu chíClient StateServer State
Sở hữuFrontend ownsBackend owns, frontend chỉ cache
Tính chấtSynchronous, predictableAsynchronous, out-of-date bất kỳ lúc nào
Ví dụUI state (modal open, theme, form input)User profile, posts, products
Tool phù hợpuseState, Zustand, ReduxReact Query, SWR, RTK Query
InvalidationKhông cầnCần refetch, cache invalidation
Loading/ErrorKhông cóLuôn cần xử lý
&9888; Anti-pattern phổ biến: Dùng Redux để fetch API → store vào global state → tự quản lý loading/error/caching/refetch. Đây là reinvent the wheel. React Query giải quyết tất cả những vấn đề này out-of-the-box.

2. Context API

Context API là built-in solution của React để pass data qua component tree mà không cần prop drilling. Tuy nhiên, nó KHÔNG phải state management tool — nó là dependency injection mechanism.

Khi nào dùng Context?

Re-render Problem

Vấn đề cốt lõi: Khi Context value thay đổi, TẤT CẢ consumers re-render — bất kể component đó có dùng phần data thay đổi hay không. Không có built-in selector mechanism.

Ví dụ gây re-render không cần thiết:

const AppContext = React.createContext({
  user: null,
  theme: "light",
  notifications: [],
});

function Header() {
  const { theme } = useContext(AppContext);
  return <View style={{ backgroundColor: theme === "dark" ? "#000" : "#fff" }} />;
}

Component Header chỉ dùng theme, nhưng khi notifications thay đổi, Header vẫn re-render vì cùng context.

Giải pháp: Split Contexts

const ThemeContext = React.createContext("light");
const UserContext = React.createContext(null);
const NotificationContext = React.createContext([]);

function AppProviders({ children }) {
  const [theme, setTheme] = useState("light");
  const [user, setUser] = useState(null);
  const [notifications, setNotifications] = useState([]);

  return (
    <ThemeContext.Provider value={theme}>
      <UserContext.Provider value={user}>
        <NotificationContext.Provider value={notifications}>
          {children}
        </NotificationContext.Provider>
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

Giờ Header chỉ subscribe ThemeContext → không re-render khi notifications thay đổi.

Tách state và dispatch: Một kỹ thuật nâng cao là tách value context (read) và dispatch context (write) để components chỉ dispatch actions không bị re-render khi state thay đổi.

const StateContext = React.createContext(null);
const DispatchContext = React.createContext(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <DispatchContext.Provider value={dispatch}>
      <StateContext.Provider value={state}>
        {children}
      </StateContext.Provider>
    </DispatchContext.Provider>
  );
}

dispatch là stable reference (không thay đổi giữa các renders), nên DispatchContext consumers không bao giờ re-render do context change.

3. Redux Toolkit (RTK)

Redux Toolkit là official, opinionated toolset cho Redux. Nó giải quyết 3 vấn đề chính của Redux gốc: quá nhiều boilerplate, cần nhiều packages bổ trợ, cấu hình phức tạp.

Store & Slice

import { configureStore, createSlice, PayloadAction } from "@reduxjs/toolkit";

interface Todo {
  id: string;
  title: string;
  completed: boolean;
}

interface TodoState {
  items: Todo[];
  filter: "all" | "active" | "completed";
}

const initialState: TodoState = {
  items: [],
  filter: "all",
};

const todoSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    addTodo(state, action: PayloadAction<{ id: string; title: string }>) {
      state.items.push({
        id: action.payload.id,
        title: action.payload.title,
        completed: false,
      });
    },
    toggleTodo(state, action: PayloadAction<string>) {
      const todo = state.items.find((t) => t.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    removeTodo(state, action: PayloadAction<string>) {
      state.items = state.items.filter((t) => t.id !== action.payload);
    },
    setFilter(state, action: PayloadAction<TodoState["filter"]>) {
      state.filter = action.payload;
    },
  },
});

export const { addTodo, toggleTodo, removeTodo, setFilter } = todoSlice.actions;

const store = configureStore({
  reducer: {
    todos: todoSlice.reducer,
  },
});

Immer tích hợp sẵn: Bên trong createSlice, bạn viết code "mutate" trực tiếp (state.items.push(...)) nhưng Immer sẽ tự tạo immutable update. Đây KHÔNG phải mutation thật — Immer tạo 1 draft proxy, track tất cả changes, rồi produce 1 new state object.

createAsyncThunk

import { createAsyncThunk } from "@reduxjs/toolkit";

const fetchTodos = createAsyncThunk(
  "todos/fetchTodos",
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch("https://jsonplaceholder.typicode.com/todos");
      if (!response.ok) {
        throw new Error("Network response was not ok");
      }
      return await response.json();
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

Xử lý trong slice với extraReducers:

const todoSlice = createSlice({
  name: "todos",
  initialState: {
    items: [],
    status: "idle",
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.status = "loading";
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.items = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.payload;
      });
  },
});

createSelector (Memoized Selectors)

import { createSelector } from "@reduxjs/toolkit";

const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;

const selectFilteredTodos = createSelector(
  [selectTodos, selectFilter],
  (todos, filter) => {
    switch (filter) {
      case "active":
        return todos.filter((t) => !t.completed);
      case "completed":
        return todos.filter((t) => t.completed);
      default:
        return todos;
    }
  }
);

createSelector (từ Reselect) chỉ tính toán lại khi inputs thay đổi. Quan trọng cho derived data — tránh re-compute mỗi render.

Middleware: Thunk vs Saga

Tiêu chíRedux ThunkRedux Saga
ComplexityThấp — chỉ là function return functionCao — cần học generator functions
Syntaxasync/await bình thườngGenerator yield (call, put, take)
TestingMock async callsDeclarative effects → dễ test hơn
CancelTự implement (AbortController)Built-in (takeLatest, race)
Complex flowsNesting callbacks/thunksElegant (channels, fork, spawn)
Bundle size~2KB~25KB
Khi nào dùngHầu hết các trường hợpComplex async flows: retry, polling, WebSocket, race conditions

Recommendation: RTK đã include Thunk middleware by default. Chỉ cần Saga khi có yêu cầu complex async orchestration. Trong 90% dự án RN, Thunk + React Query đủ dùng.

Redux Persist

import AsyncStorage from "@react-native-async-storage/async-storage";
import { persistStore, persistReducer } from "redux-persist";

const persistConfig = {
  key: "root",
  storage: AsyncStorage,
  whitelist: ["auth", "settings"],
  blacklist: ["ui"],
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ["persist/PERSIST", "persist/REHYDRATE"],
      },
    }),
});

const persistor = persistStore(store);

whitelist chỉ persist các slices cần thiết (auth token, user settings). blacklist loại bỏ UI state không cần lưu. Luôn dùng AsyncStorage cho React Native thay vì localStorage.

4. Zustand

Tại sao Zustand lightweight?

Basic Zustand Store

import { create } from "zustand";

interface TodoStore {
  items: Todo[];
  filter: "all" | "active" | "completed";
  addTodo: (title: string) => void;
  toggleTodo: (id: string) => void;
  removeTodo: (id: string) => void;
  setFilter: (filter: TodoStore["filter"]) => void;
}

const useTodoStore = create<TodoStore>((set) => ({
  items: [],
  filter: "all",

  addTodo: (title) =>
    set((state) => ({
      items: [
        ...state.items,
        { id: Date.now().toString(), title, completed: false },
      ],
    })),

  toggleTodo: (id) =>
    set((state) => ({
      items: state.items.map((t) =>
        t.id === id ? { ...t, completed: !t.completed } : t
      ),
    })),

  removeTodo: (id) =>
    set((state) => ({
      items: state.items.filter((t) => t.id !== id),
    })),

  setFilter: (filter) => set({ filter }),
}));

Sử dụng trong Component

function TodoList() {
  const items = useTodoStore((state) => state.items);
  const toggleTodo = useTodoStore((state) => state.toggleTodo);

  return (
    <FlatList
      data={items}
      renderItem={({ item }) => (
        <Pressable onPress={() => toggleTodo(item.id)}>
          <Text>{item.title}</Text>
        </Pressable>
      )}
    />
  );
}

Mỗi useTodoStore call với selector riêng → component chỉ re-render khi phần data nó subscribe thực sự thay đổi. Đây là lợi thế lớn so với Context API.

Persist & Devtools Middleware

import { create } from "zustand";
import { persist, devtools } from "zustand/middleware";
import AsyncStorage from "@react-native-async-storage/async-storage";

const useTodoStore = create<TodoStore>()(
  devtools(
    persist(
      (set) => ({
        items: [],
        filter: "all",
        addTodo: (title) =>
          set((state) => ({
            items: [
              ...state.items,
              { id: Date.now().toString(), title, completed: false },
            ],
          })),
      }),
      {
        name: "todo-storage",
        storage: {
          getItem: async (name) => {
            const value = await AsyncStorage.getItem(name);
            return value ? JSON.parse(value) : null;
          },
          setItem: async (name, value) => {
            await AsyncStorage.setItem(name, JSON.stringify(value));
          },
          removeItem: async (name) => {
            await AsyncStorage.removeItem(name);
          },
        },
      }
    )
  )
);

Shopping Cart Example

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  addItem: (product: Omit<CartItem, "quantity">) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
  totalPrice: () => number;
  totalItems: () => number;
}

const useCartStore = create<CartStore>((set, get) => ({
  items: [],

  addItem: (product) =>
    set((state) => {
      const existing = state.items.find((i) => i.id === product.id);
      if (existing) {
        return {
          items: state.items.map((i) =>
            i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
          ),
        };
      }
      return { items: [...state.items, { ...product, quantity: 1 }] };
    }),

  removeItem: (id) =>
    set((state) => ({
      items: state.items.filter((i) => i.id !== id),
    })),

  updateQuantity: (id, quantity) =>
    set((state) => ({
      items:
        quantity <= 0
          ? state.items.filter((i) => i.id !== id)
          : state.items.map((i) =>
              i.id === id ? { ...i, quantity } : i
            ),
    })),

  clearCart: () => set({ items: [] }),

  totalPrice: () =>
    get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),

  totalItems: () =>
    get().items.reduce((sum, item) => sum + item.quantity, 0),
}));

Code Comparison: Redux Toolkit vs Zustand

AspectRedux ToolkitZustand
Setup filesstore.ts + slice.ts + Provider wrapper1 file, không cần Provider
Boilerplate~50-80 lines cho basic CRUD~20-30 lines
AsynccreateAsyncThunk + extraReducersGọi async trực tiếp trong action
SelectorscreateSelector (Reselect)Inline selector function
DevToolsBuilt-in (Redux DevTools)Middleware (cũng dùng Redux DevTools)
MiddlewareHệ sinh thái lớn (Saga, Persist, Logger)Ít hơn nhưng đủ dùng
Learning curveCao — concepts nhiều (actions, reducers, thunks)Thấp — giống useState on steroids
Team scaleTốt cho large team (strict patterns)Tốt cho small-medium team

5. TanStack Query (React Query)

React Query KHÔNG phải state management — nó là async state manager chuyên cho server state. Nó handle: fetching, caching, synchronizing, background updates, pagination, optimistic updates — tất cả những gì bạn tự implement bằng Redux + 200 lines boilerplate.

staleTime vs gcTime

ConfigMô tảDefaultÝ nghĩa thực tế
staleTimeThời gian data được coi là fresh0 (luôn stale)Trong khoảng này, React Query trả data từ cache mà KHÔNG refetch
gcTimeThời gian data ở trong cache sau khi không còn observer nào5 phútSau thời gian này, data bị garbage collect khỏi cache

Mental model: staleTime = 30000 nghĩa là trong 30 giây đầu, navigate đi rồi quay lại sẽ thấy data ngay (từ cache) mà không có loading spinner. Sau 30s, data được đánh dấu stale → lần mount tiếp theo sẽ refetch ở background (nhưng vẫn show cache trước).

useQuery

import { useQuery } from "@tanstack/react-query";

interface User {
  id: string;
  name: string;
  email: string;
}

function useUser(userId: string) {
  return useQuery<User>({
    queryKey: ["user", userId],
    queryFn: async () => {
      const res = await fetch(`https://api.example.com/users/${userId}`);
      if (!res.ok) throw new Error("Failed to fetch user");
      return res.json();
    },
    staleTime: 5 * 60 * 1000,
    retry: 2,
  });
}

function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, isError, error } = useUser(userId);

  if (isLoading) return <ActivityIndicator />;
  if (isError) return <Text>Error: {error.message}</Text>;

  return <Text>{data.name}</Text>;
}

useMutation

import { useMutation, useQueryClient } from "@tanstack/react-query";

function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (userData: Partial<User> & { id: string }) => {
      const res = await fetch(`https://api.example.com/users/${userData.id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(userData),
      });
      if (!res.ok) throw new Error("Failed to update");
      return res.json();
    },
    onSuccess: (data, variables) => {
      queryClient.invalidateQueries({ queryKey: ["user", variables.id] });
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });
}

useInfiniteQuery

import { useInfiniteQuery } from "@tanstack/react-query";

function useInfinitePosts() {
  return useInfiniteQuery({
    queryKey: ["posts"],
    queryFn: async ({ pageParam = 1 }) => {
      const res = await fetch(
        `https://api.example.com/posts?page=${pageParam}&limit=20`
      );
      return res.json();
    },
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.hasMore ? allPages.length + 1 : undefined;
    },
    initialPageParam: 1,
  });
}

function PostFeed() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useInfinitePosts();

  const allPosts = data?.pages.flatMap((page) => page.items) ?? [];

  return (
    <FlatList
      data={allPosts}
      onEndReached={() => hasNextPage && fetchNextPage()}
      onEndReachedThreshold={0.5}
      ListFooterComponent={
        isFetchingNextPage ? <ActivityIndicator /> : null
      }
      renderItem={({ item }) => <PostCard post={item} />}
    />
  );
}

Optimistic Updates

function useLikePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (postId: string) =>
      fetch(`/api/posts/${postId}/like`, { method: "POST" }),

    onMutate: async (postId) => {
      await queryClient.cancelQueries({ queryKey: ["posts", postId] });

      const previousPost = queryClient.getQueryData(["posts", postId]);

      queryClient.setQueryData(["posts", postId], (old: Post) => ({
        ...old,
        likes: old.likes + 1,
        isLiked: true,
      }));

      return { previousPost };
    },

    onError: (_error, postId, context) => {
      queryClient.setQueryData(["posts", postId], context?.previousPost);
    },

    onSettled: (_data, _error, postId) => {
      queryClient.invalidateQueries({ queryKey: ["posts", postId] });
    },
  });
}

Flow: User tap like → UI update ngay lập tức (optimistic) → gửi API request → nếu fail, rollback về state cũ → cuối cùng luôn invalidate để đồng bộ với server.

Offline Support

React Query + Offline:

6. State Normalization

Vấn đề: Khi API trả nested data (post chứa author, author chứa avatar, comments chứa users...), nếu lưu nguyên → data trùng lặp, update 1 chỗ phải tìm update nhiều chỗ.

Giải pháp: Normalize thành flat structure, dùng IDs để reference.

interface NormalizedState {
  users: Record<string, User>;
  posts: Record<string, Post>;
  comments: Record<string, Comment>;
}

const normalizedState: NormalizedState = {
  users: {
    u1: { id: "u1", name: "Alice" },
    u2: { id: "u2", name: "Bob" },
  },
  posts: {
    p1: { id: "p1", authorId: "u1", commentIds: ["c1", "c2"] },
  },
  comments: {
    c1: { id: "c1", authorId: "u2", text: "Great post!" },
    c2: { id: "c2", authorId: "u1", text: "Thanks!" },
  },
};

Khi update user u1 name, chỉ cần update 1 chỗ → tất cả nơi reference u1 tự động nhận data mới.

RTK EntityAdapter: Redux Toolkit cung cấp createEntityAdapter để tự động normalize entities với các CRUD operations đã built-in (addOne, updateOne, removeOne, upsertMany...).

7. Bảng so sánh tổng hợp

Tiêu chíuseStateContextRedux (RTK)ZustandReact Query
Loại stateLocalShared (tĩnh)Client globalClient globalServer state
Scope1 componentSubtreeToàn appToàn appToàn app
Re-renderComponent đóTất cả consumersConnected componentsChỉ subscribedChỉ observer queries
BoilerplateCực thấpThấpTrung bìnhThấpThấp
DevToolsReact DevToolsReact DevToolsRedux DevToolsRedux DevToolsRQ DevTools
PersistKhôngKhôngredux-persistMiddlewarepersistQueryClient
AsyncTự handleTự handlecreateAsyncThunkTrực tiếpBuilt-in
CacheKhôngKhôngTự implementTự implementTự động
Best forForm, toggle, UITheme, localeComplex client logicShared client stateAPI data

8. Khi nào Local vs Lift Up vs Global?

Decision tree:

Rule of thumb: Bắt đầu với local state (useState). Chỉ escalate lên level tiếp theo khi thật sự cần. Premature globalization là anti-pattern phổ biến nhất.

&10067; Câu hỏi phỏng vấn + Đáp án

🛠️ Debug playbook: state bug

Triệu chứngKiểm traFix thường gặp
Screen re-render khi state không liên quan đổiSelector trả gì, component subscribe store nào, object reference mới khôngSelector nhỏ hơn, shallow compare, split store/context
Data API cũ hiển thị sau refetchServer state đang ở store nào, query key đúng không, staleTime/gcTimeĐưa server data về React Query, thiết kế query key rõ
App mở lại ở trạng thái lỗi/loadingPersist payload có loading/error khôngPersist whitelist, bỏ transient state
Optimistic update sai sau API failCó snapshot rollback không, invalidate sau mutation chưaSnapshot trong onMutate, rollback onError, refetch onSettled

Khung trả lời phỏng vấn: Đừng bắt đầu bằng tên tool. Bắt đầu bằng phân loại state: owner, lifetime, scope, sync. Sau đó mới nói vì sao chọn local state, Context, Zustand, Redux Toolkit hoặc React Query.

Q1: Client State vs Server State — khác nhau thế nào? Tại sao phân biệt quan trọng?

Client State là state mà frontend sở hữu hoàn toàn: UI toggles, form inputs, selected tab, theme. Nó synchronous, predictable, không bao giờ out-of-date.

Server State là data sống trên backend: user profiles, posts, products. Frontend chỉ cache 1 snapshot — data có thể thay đổi bất kỳ lúc nào bởi user khác.

Tại sao quan trọng: Mỗi loại cần strategy khác nhau. Server state cần: caching, background refetch, stale detection, error/retry, optimistic updates. Dùng Redux thuần cho server state nghĩa là tự implement tất cả những thứ đó — React Query giải quyết hết trong <10 lines.

Q2: Context API có thể thay thế Redux không? Khi nào không nên dùng Context?

Context KHÔNG phải state management. Nó là dependency injection — một cách để truyền data qua tree mà không prop drilling.

Không nên dùng khi:

  • State thay đổi thường xuyên (mỗi keystroke, animation frame) → all consumers re-render
  • Nhiều consumers chỉ cần 1 phần nhỏ data → không có selector mechanism
  • Cần time-travel debugging, middleware, action logging

Nên dùng khi: Data thay đổi ít (theme toggle, locale switch, auth status). Trong trường hợp này, Context đơn giản và đủ tốt.

Q3: Giải thích cách Immer hoạt động trong Redux Toolkit. Tại sao có thể "mutate" state?

Immer sử dụng Proxy (ES6) để tạo 1 draft object. Khi bạn viết state.items.push(newItem), bạn thực ra đang mutate draft — không phải state thật. Immer track tất cả changes trên draft, rồi produce 1 new immutable state object bằng cách structural sharing (chỉ copy các phần thay đổi, phần không đổi reuse reference cũ).

Lưu ý quan trọng: Trong createSlice reducer, bạn hoặc mutate draft (Immer handle) HOẶC return new state — KHÔNG LÀM CẢ HAI. Nếu return value, Immer bỏ qua mutations.

Q4: createSelector dùng để làm gì? Khi nào cần memoized selectors?

createSelector tạo memoized selector — chỉ recalculate khi input selectors trả về giá trị khác (so sánh bằng ===).

Cần khi: Derived data computation tốn kém. Ví dụ: filter + sort 1000 items, compute totals, transform data shape. Nếu không memoize, mỗi lần bất kỳ state nào thay đổi → re-compute → re-render tất cả connected components.

Không cần khi: Selector chỉ đọc trực tiếp 1 field (state.user.name) — không có computation nào cần cache.

Q5: Zustand khác Redux ở điểm nào? Khi nào chọn cái nào?

Khác biệt chính:

  • Architecture: Redux theo flux pattern (action → reducer → store). Zustand là mutable store bên ngoài React tree
  • Provider: Redux cần Provider wrapper. Zustand không cần
  • Subscription: Zustand dùng selector-based subscription natively — chỉ re-render khi selected data thay đổi
  • Boilerplate: Zustand ít hơn 60-70%

Chọn Redux khi: Team lớn, cần strict patterns, đã có Redux ecosystem (Saga, Persist), cần time-travel debugging mạnh

Chọn Zustand khi: Team nhỏ-vừa, cần setup nhanh, ít boilerplate, app không quá phức tạp

Q6: staleTime vs gcTime (cacheTime) trong React Query?

staleTime (default: 0): Thời gian data được coi là "fresh". Trong khoảng này, React Query trả data từ cache mà KHÔNG trigger background refetch. Sau khoảng này, data đánh dấu "stale" → lần sử dụng tiếp theo sẽ refetch ở background.

gcTime (default: 5 phút): Thời gian data tồn tại trong cache SAU KHI không còn component nào observe query đó. Hết thời gian này → data bị xóa khỏi cache hoàn toàn.

Ví dụ: staleTime: 60000, gcTime: 300000 → Data fresh 1 phút (không refetch), cache sống 5 phút sau khi component unmount.

Q7: Optimistic Update là gì? Implement như thế nào với React Query?

Optimistic Update: Update UI ngay lập tức trước khi server confirm → UX mượt hơn, cảm giác instant.

Steps:

  1. onMutate: Cancel outgoing queries, save current data (snapshot), update cache optimistically
  2. onError: Rollback về snapshot nếu API fail
  3. onSettled: Invalidate queries để sync với server (bất kể success/error)

Use cases: Like/unlike, follow/unfollow, add to cart, toggle bookmark — actions mà user expect instant feedback.

Q8: Redux middleware hoạt động như thế nào?

Middleware trong Redux là higher-order function nằm giữa dispatch và reducer. Mỗi action đi qua chain of middleware trước khi tới reducer.

Signature: store => next => action => { ... }

Ứng dụng:

  • Thunk: Cho phép dispatch functions (thay vì chỉ plain objects) → async logic
  • Logger: Log mỗi action + state before/after
  • Saga: Listen actions, trigger complex async flows via generators
  • Persist: Serialize state vào storage khi state thay đổi
Q9: Tại sao cần State Normalization? Dùng khi nào?

Vấn đề không normalize: Data trùng lặp (cùng user xuất hiện trong posts, comments, followers). Update tên user → phải tìm và update TẤT CẢ nơi chứa user đó. Dễ bỏ sót → inconsistent UI.

Normalize: Lưu mỗi entity type trong flat Record (id → entity). Relationships dùng ID references. Update 1 entity → chỉ 1 chỗ → tất cả nơi reference tự nhận giá trị mới.

Dùng khi: Entities có relationships (users, posts, comments), cùng entity xuất hiện nhiều nơi, cần update real-time. Không cần khi: Data đơn giản, flat, không relationships.

Q10: useInfiniteQuery hoạt động thế nào? So với tự implement pagination?

useInfiniteQuery quản lý paginated data dưới dạng mảng các pages. Mỗi page là 1 query result riêng.

So với tự implement:

  • Tự implement: useEffect + useState cho page number, loading, error, concatenate data → ~30-50 lines, dễ bug (race conditions, duplicate fetch)
  • useInfiniteQuery: handle pagination, caching, deduplication, refetching per-page, bi-directional scrolling → ~10 lines

Key config: getNextPageParam return page param tiếp theo (hoặc undefined nếu hết data). Kết hợp với FlatList onEndReached trong RN.

Q11: Redux Persist hoạt động thế nào? Whitelist vs Blacklist?

Redux Persist serialize Redux state vào storage (AsyncStorage cho RN) và rehydrate khi app khởi động.

Whitelist: Chỉ định rõ slices nào CẦN persist (ví dụ: auth, settings). An toàn hơn — thêm slice mới mặc định không persist.

Blacklist: Chỉ định slices KHÔNG persist (ví dụ: ui, temp). Rủi ro hơn — thêm slice mới mặc định persist.

Best practice: Dùng whitelist. Chỉ persist auth tokens, user preferences, offline data. KHÔNG persist loading states, error messages, ephemeral UI state.

Q12: Khi nào dùng local state (useState), khi nào global state?

Local state (useState):

  • Chỉ 1 component cần (form input, toggle, animation value)
  • State không cần survive navigation
  • State reset khi component unmount là OK

Lift up: 2-3 sibling components cần share → đưa lên common parent. Stop khi prop drilling > 2 levels.

Global (Zustand/Redux):

  • Nhiều unrelated components cần access (shopping cart badge + cart screen + checkout)
  • State cần persist across navigation
  • Complex state transitions (multi-step form, game state)

Nguyên tắc: Bắt đầu local. Chỉ escalate khi pain point thật sự xuất hiện. Premature globalization → unnecessary complexity + re-renders.

&127947; Bài tập

Bài 1: Mini Redux (createStore)
Implement function createStore từ đầu bằng TypeScript: Acceptance criteria:
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
}

const store = createStore(counterReducer);
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "DECREMENT" });
unsubscribe();
store.dispatch({ type: "INCREMENT" });

Expected output: { count: 1 }{ count: 2 }{ count: 1 }. Lần dispatch cuối không log vì đã unsubscribe.

Giải Bài 1:
type Reducer<S, A> = (state: S, action: A) => S;
type Listener = () => void;

function createStore<S, A>(reducer: Reducer<S, A>, initialState?: S) {
  let state = initialState ?? reducer(undefined as S, { type: "@@INIT" } as A);
  let listeners: Listener[] = [];

  function getState(): S {
    return state;
  }

  function dispatch(action: A): void {
    state = reducer(state, action);
    listeners.forEach((listener) => listener());
  }

  function subscribe(listener: Listener): () => void {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter((l) => l !== listener);
    };
  }

  return { getState, dispatch, subscribe };
}
Bài 2: Pub/Sub Event System
Implement EventEmitter class: Acceptance criteria:
const emitter = new EventEmitter();

emitter.on("data", (x) => console.log("A:", x));
emitter.once("data", (x) => console.log("B:", x));

emitter.emit("data", 42);
emitter.emit("data", 99);

Expected: Lần emit đầu: "A: 42" + "B: 42". Lần emit thứ 2: chỉ "A: 99" (B đã unsubscribe).

Giải Bài 2:
type Callback = (...args: unknown[]) => void;

class EventEmitter {
  private events: Map<string, Callback[]> = new Map();

  on(event: string, callback: Callback): void {
    const callbacks = this.events.get(event) ?? [];
    callbacks.push(callback);
    this.events.set(event, callbacks);
  }

  off(event: string, callback: Callback): void {
    const callbacks = this.events.get(event);
    if (!callbacks) return;
    this.events.set(
      event,
      callbacks.filter((cb) => cb !== callback)
    );
  }

  emit(event: string, ...args: unknown[]): void {
    const callbacks = this.events.get(event);
    if (!callbacks) return;
    callbacks.forEach((cb) => cb(...args));
  }

  once(event: string, callback: Callback): void {
    const wrapper: Callback = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }
}
Bài 3: Shopping Cart với Zustand
Build shopping cart store với Zustand: Acceptance criteria:

&128187; Mini exercise thực tế React Native

Exercise 1: Cart store có persist
Dùng Zustand hoặc Redux Toolkit để build cart: add/remove/update quantity/apply coupon. Persist cart, nhưng không persist loading/error tạm thời.
Exercise 2: Product list dùng server state
Dùng TanStack Query cho list/detail/infinite scroll. Khi update product yêu thích, optimistic update detail và list, rollback nếu API fail.
Exercise 3: Refactor sai lầm phổ biến
Bắt đầu từ một app nhét API data vào Redux. Refactor: server data sang TanStack Query, UI state giữ local/Zustand. Viết notes giải thích state nào đi đâu.

&9888; Lỗi thường gặp

&127970; Case đi làm thật

Case: App thương mại điện tử lưu cart trong global store và product detail cũng trong cùng store. Sau khi user đổi coupon, product detail screen re-render hàng loạt dù không liên quan.

Cách xử lý: Tách cart store riêng, product data dùng TanStack Query, selector chỉ trả primitive hoặc memoized object. Đo bằng React DevTools Profiler trước/sau khi tách.

&129504; Ghi nhớ nhanh

Đừng hỏi “dùng Redux hay Zustand?”. Hỏi trước: state này ai sở hữu, sống bao lâu, bao nhiêu screen cần, có cần refetch/persist/debug không.

&10067; Câu hỏi tự kiểm tra

1. Vì sao server state không nên mặc định đưa vào Redux/Zustand?
2. Khi persist store, bạn sẽ loại bỏ những field nào?
3. Một screen re-render khi cart thay đổi dù không dùng cart. Bạn debug theo thứ tự nào?
4. Optimistic update cần snapshot gì để rollback an toàn?

&9989; Checklist tự đánh giá

&128206; Tài nguyên

&128216; Redux Toolkit Official Docs — Quick Start, API Reference, Tutorials
&128216; Zustand Documentation — Official docs, recipes, middleware
&128216; TanStack Query (React Query) — Guides, API Reference
&128250; "Zustand: Bear necessities for state management" — Jack Herrington (YouTube)
&128250; "React Query in 100 Seconds" — Fireship (YouTube)
&128221; Practical React Query — TkDodo blog series, best practices cho React Query
&128221; Redux Style Guide — Official best practices và patterns
&128221; "Application State Management with React" — Kent C. Dodds
&128216; React.dev — Managing State — Official React docs về state management