useState hoặc useReducer.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 State | Server State |
|---|---|---|
| Sở hữu | Frontend owns | Backend owns, frontend chỉ cache |
| Tính chất | Synchronous, predictable | Asynchronous, 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ợp | useState, Zustand, Redux | React Query, SWR, RTK Query |
| Invalidation | Không cần | Cần refetch, cache invalidation |
| Loading/Error | Không có | Luôn cần xử lý |
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.
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.
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.
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.
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.
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;
});
},
});
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.
| Tiêu chí | Redux Thunk | Redux Saga |
|---|---|---|
| Complexity | Thấp — chỉ là function return function | Cao — cần học generator functions |
| Syntax | async/await bình thường | Generator yield (call, put, take) |
| Testing | Mock async calls | Declarative effects → dễ test hơn |
| Cancel | Tự implement (AbortController) | Built-in (takeLatest, race) |
| Complex flows | Nesting callbacks/thunks | Elegant (channels, fork, spawn) |
| Bundle size | ~2KB | ~25KB |
| Khi nào dùng | Hầu hết các trường hợp | Complex 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.
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.
Tại sao Zustand lightweight?
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 }),
}));
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.
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);
},
},
}
)
)
);
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),
}));
| Aspect | Redux Toolkit | Zustand |
|---|---|---|
| Setup files | store.ts + slice.ts + Provider wrapper | 1 file, không cần Provider |
| Boilerplate | ~50-80 lines cho basic CRUD | ~20-30 lines |
| Async | createAsyncThunk + extraReducers | Gọi async trực tiếp trong action |
| Selectors | createSelector (Reselect) | Inline selector function |
| DevTools | Built-in (Redux DevTools) | Middleware (cũng dùng Redux DevTools) |
| Middleware | Hệ sinh thái lớn (Saga, Persist, Logger) | Ít hơn nhưng đủ dùng |
| Learning curve | Cao — concepts nhiều (actions, reducers, thunks) | Thấp — giống useState on steroids |
| Team scale | Tốt cho large team (strict patterns) | Tốt cho small-medium team |
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.
| Config | Mô tả | Default | Ý nghĩa thực tế |
|---|---|---|---|
staleTime | Thời gian data được coi là fresh | 0 (luôn stale) | Trong khoảng này, React Query trả data từ cache mà KHÔNG refetch |
gcTime | Thời gian data ở trong cache sau khi không còn observer nào | 5 phút | Sau 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).
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>;
}
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"] });
},
});
}
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} />}
/>
);
}
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.
React Query + Offline:
networkMode: "offlineFirst" → trả cache data ngay cả khi offlineonlineManager → detect online/offline statuspersistQueryClient để lưu cache vào AsyncStorage → app hoạt động được hoàn toàn offline (với data đã cache)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...).
| Tiêu chí | useState | Context | Redux (RTK) | Zustand | React Query |
|---|---|---|---|---|---|
| Loại state | Local | Shared (tĩnh) | Client global | Client global | Server state |
| Scope | 1 component | Subtree | Toàn app | Toàn app | Toàn app |
| Re-render | Component đó | Tất cả consumers | Connected components | Chỉ subscribed | Chỉ observer queries |
| Boilerplate | Cực thấp | Thấp | Trung bình | Thấp | Thấp |
| DevTools | React DevTools | React DevTools | Redux DevTools | Redux DevTools | RQ DevTools |
| Persist | Không | Không | redux-persist | Middleware | persistQueryClient |
| Async | Tự handle | Tự handle | createAsyncThunk | Trực tiếp | Built-in |
| Cache | Không | Không | Tự implement | Tự implement | Tự động |
| Best for | Form, toggle, UI | Theme, locale | Complex client logic | Shared client state | API data |
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.
| Triệu chứng | Kiểm tra | Fix thường gặp |
|---|---|---|
| Screen re-render khi state không liên quan đổi | Selector trả gì, component subscribe store nào, object reference mới không | Selector nhỏ hơn, shallow compare, split store/context |
| Data API cũ hiển thị sau refetch | Server 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/loading | Persist payload có loading/error không | Persist whitelist, bỏ transient state |
| Optimistic update sai sau API fail | Có snapshot rollback không, invalidate sau mutation chưa | Snapshot 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.
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.
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:
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.
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.
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.
Khác biệt chính:
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
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.
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:
onMutate: Cancel outgoing queries, save current data (snapshot), update cache optimisticallyonError: Rollback về snapshot nếu API failonSettled: 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.
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:
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.
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:
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.
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.
Local state (useState):
Lift up: 2-3 sibling components cần share → đưa lên common parent. Stop khi prop drilling > 2 levels.
Global (Zustand/Redux):
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.
createStore từ đầu bằng TypeScript:
getState(): trả current statedispatch(action): gọi reducer với current state + action, update statesubscribe(listener): register listener, trả về unsubscribe functionfunction 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.
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 };
}
EventEmitter class:
on(event, callback): subscribeoff(event, callback): unsubscribeemit(event, ...args): trigger all callbacks cho event đóonce(event, callback): subscribe nhưng tự unsubscribe sau lần trigger đầu tiênconst 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).
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);
}
}
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.
Đừ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.