Quản lý state trong React: từ useState đến Zustand và Redux
Bài viết hướng dẫn quản lý state trong React từ cơ bản tới nâng cao: bắt đầu với useState và lift state up, sau đó là Context API, useReducer, rồi mới tới thư viện ngoài như Zustand hay Redux. Quan trọng là chọn đúng công cụ cho từng tình huống thay vì dùng Redux sớm một cách không cần thiết, vì phần lớn app vừa và nhỏ đã đủ dùng React thuần.
Khi mới học React, bạn quản lý state bằng useState và mọi thứ chạy ngon. Nhưng app lớn dần lên: một dữ liệu cần dùng ở nhiều chỗ, component cha truyền props xuống năm bảy tầng, rồi đến lúc bạn không còn biết "dữ liệu này thay đổi ở đâu, vì sao nó sai". Đó là lúc câu hỏi quản lý state trong React trở nên nghiêm túc.
Bài này đi từ thấp lên cao: bắt đầu với công cụ React có sẵn, rồi mới tới thư viện ngoài như Zustand và Redux. Quan trọng nhất: bạn sẽ biết khi nào cần cái nào, để không lôi Redux vào một dự án mà useState đã đủ.
State là gì và vì sao app lớn thì khó
State là dữ liệu thay đổi theo thời gian của một component: số sản phẩm trong giỏ hàng, người dùng đã đăng nhập chưa, modal đang mở hay đóng. Khi state đổi, React tự vẽ lại phần giao diện liên quan. Nếu bạn còn mơ hồ phần này, đọc lại bài React là gì trước đã.
Vấn đề bắt đầu khi app phình to. Theo chính tài liệu react.dev, phần lớn bug state đến từ việc lưu dữ liệu thừa (redundant state) và để dữ liệu bị lệch nhau giữa các nơi. Hai triệu chứng kinh điển:
- Trùng lặp dữ liệu: cùng một thông tin lưu ở hai chỗ, sửa chỗ này quên chỗ kia, giao diện hiển thị sai.
- Prop drilling: một dữ liệu phải truyền props qua hàng loạt component trung gian chỉ để tới được component sâu bên dưới cần dùng nó.
Tin tốt: bạn không cần thư viện ngay. React đưa ra giải pháp theo từng cấp, dùng đúng cấp là đủ.
Cấp 1: useState và lift state up
Mặc định, mỗi state nên nằm trong component cần nó. Khi hai component anh em cần dùng chung một state, bạn đẩy state đó lên component cha gần nhất rồi truyền xuống bằng props. Kỹ thuật này react.dev gọi là lifting state up.
Ví dụ một accordion chỉ cho mở một panel tại một thời điểm. State "panel nào đang mở" sống ở component cha:
function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<Panel isActive={activeIndex === 0} onShow={() => setActiveIndex(0)}>
Nội dung 1
</Panel>
<Panel isActive={activeIndex === 1} onShow={() => setActiveIndex(1)}>
Nội dung 2
</Panel>
</>
);
}Một mẹo nữa để tránh state thừa: đừng lưu cái có thể tính ra được. Có firstName và lastName rồi thì fullName chỉ cần tính trong lúc render, không cần useState riêng. Bớt một state là bớt một nguồn bug.
Phần lớn app vừa và nhỏ, dừng ở đây là đủ. Đừng vội phức tạp hóa.
Cấp 2: Context để hết prop drilling
Lift state up bị đuối khi cây component quá sâu: bạn phải truyền props qua một loạt tầng trung gian chẳng dùng tới nó. Đây đúng là bài toán Context API sinh ra để giải.
Context cho component cha cung cấp một giá trị cho toàn bộ cây bên dưới, sâu bao nhiêu cũng được, mà không cần truyền props từng tầng. Ba bước theo react.dev:
// 1. Tạo context
import { createContext, useContext } from 'react';
export const ThemeContext = createContext('light');
// 2. Bọc cây component bằng Provider
function App() {
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}
// 3. Component sâu bên dưới đọc trực tiếp, khỏi cần props
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>Bấm</button>;
}Một lưu ý quan trọng mà react.dev nhấn mạnh: đừng dùng Context quá sớm. Trước khi với tới nó, hãy thử truyền props bình thường (dòng dữ liệu rõ ràng hơn), hoặc tách component và truyền JSX qua prop children để bớt tầng trung gian. Context hợp nhất với dữ liệu mang tính "toàn cục nhẹ": theme, thông tin user đăng nhập, ngôn ngữ.
Cấp 3: useReducer khi logic cập nhật phức tạp
useState rời rạc bắt đầu khó chịu khi một nhóm state liên quan đến nhau và có nhiều kiểu cập nhật (thêm, sửa, xóa, toggle). useReducer gom toàn bộ logic cập nhật vào một hàm thuần (pure function) duy nhất, còn component chỉ việc dispatch một action mô tả "chuyện gì vừa xảy ra".
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added':
return [...tasks, { id: action.id, text: action.text, done: false }];
case 'deleted':
return tasks.filter((t) => t.id !== action.id);
default:
throw Error('Action không hợp lệ: ' + action.type);
}
}
function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, []);
// dispatch({ type: 'added', id: 1, text: 'Học React' });
}useReducer là một trong những hook hay bị bỏ quên, bạn có thể xem thêm ở bài React Hooks. Điểm hay: logic cập nhật state nằm tách khỏi giao diện nên dễ đọc và dễ test.
Cấp 4: Reducer + Context, bộ đôi React khuyến nghị
Khi state phức tạp (cần useReducer) lại còn phải dùng ở nhiều nơi xa nhau (cần Context), react.dev khuyên ghép cả hai. Bài "Scaling Up with Reducer and Context" mô tả đúng cách làm này:
// TasksContext.js
import { createContext, useContext, useReducer } from 'react';
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, []);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
export const useTasks = () => useContext(TasksContext);
export const useTasksDispatch = () => useContext(TasksDispatchContext);Lý do tách state và dispatch ra hai context: dispatch gần như không đổi, nên component nào chỉ dùng dispatch sẽ không bị render lại vô ích mỗi khi state đổi. Custom hook useTasks / useTasksDispatch cho bạn một API gọn để các component dùng.
Bộ đôi này gánh được khá nhiều. Nhiều app trung bình không cần thư viện ngoài nếu khéo dùng nó.
Khi nào mới thực sự cần thư viện ngoài
Reducer + Context có hai điểm yếu khi app rất lớn: hiệu năng re-render cần tự tối ưu, và bạn phải tự dựng dần một "kiến trúc state" thủ công. Cân nhắc thư viện khi:
- State global nhiều, đổi liên tục, và nhiều component ở xa nhau cùng cần.
- Logic cập nhật phức tạp, cần công cụ debug như theo dõi lịch sử thay đổi (time-travel).
- Codebase trung bình tới lớn, nhiều người cùng làm, cần một quy ước chung.
Hai cái tên phổ biến nhất là Zustand và Redux Toolkit.
Zustand là gì
Zustand là một thư viện quản lý state nhỏ gọn, nhanh, API dựa trên hook. Điểm cuốn hút: store chính là một hook, không cần bọc Provider quanh app, và component chỉ render lại khi đúng phần state nó chọn (qua selector) thay đổi.
import { create } from 'zustand';
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
}));
// Dùng ở bất kỳ component nào, không cần Provider
function CartBadge() {
const count = useCartStore((state) => state.items.length);
return <span>Giỏ hàng: {count}</span>;
}So với Context, Zustand đỡ hẳn vụ Provider lồng nhau và vấn đề re-render thừa. So với Redux, nó ít boilerplate hơn nhiều. Đây là lựa chọn rất hợp lý cho phần lớn dự án thực tế hiện nay.
Redux là gì (và Redux Toolkit)
Redux là một predictable state container: toàn bộ state app nằm trong một store duy nhất, chỉ thay đổi qua action và reducer thuần. Triết lý của nó là trả lời được câu hỏi "slice state này đổi lúc nào, dữ liệu từ đâu tới", nên dễ debug và lần ngược lịch sử.
Đổi lại, Redux thuần nổi tiếng nhiều boilerplate. Redux Toolkit (RTK) là cách viết Redux chính thức hiện nay, sinh ra để dẹp đúng vấn đề đó: cấu hình store rườm rà và viết quá nhiều code.
import { createSlice, configureStore } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addItem: (state, action) => { state.items.push(action.payload); },
clear: (state) => { state.items = []; },
},
});
export const { addItem, clear } = cartSlice.actions;
export const store = configureStore({ reducer: { cart: cartSlice.reducer } });Redux mạnh ở app lớn, nhiều người, state phức tạp và cần công cụ dev mạnh. Nhưng chính tài liệu Redux trích lời Dan Abramov (tác giả): "đừng dùng Redux cho tới khi bạn gặp vấn đề với React thuần". Nếu chưa chắc có cần Redux không, nghĩa là bạn chưa cần.
Bảng so sánh: Context vs Zustand vs Redux
| Tiêu chí | Context + useReducer | Zustand | Redux Toolkit |
|---|---|---|---|
| Cài thêm thư viện | Không, có sẵn trong React | Có, rất nhẹ | Có, nặng hơn |
| Cần Provider bọc app | Có | Không | Có |
| Lượng boilerplate | Vừa | Thấp | Trung bình (đã gọn nhờ RTK) |
| Tối ưu re-render | Tự lo | Tự động qua selector | Tốt qua selector |
| Dev tools, time-travel | Không sẵn | Cơ bản | Mạnh, hệ sinh thái lớn |
| Đường học | Dễ, kiến thức React thuần | Dễ | Dốc hơn |
| Hợp với | App vừa, state global nhẹ | Đa số dự án thực tế | App lớn, nhiều người, state phức tạp |
Lời khuyên cho người mới: đừng lạm dụng Redux sớm
Sai lầm phổ biến của người mới là cài Redux ngay từ dự án đầu vì "thấy ai cũng nhắc tới". Kết quả: viết gấp đôi lượng code cho một todo list mà useState đã dư sức làm.
Lộ trình lành mạnh đi từ dưới lên:
- Mặc định dùng
useState, cần dùng chung thì lift state up. - Prop drilling khó chịu thì thêm Context cho dữ liệu toàn cục nhẹ.
- Logic cập nhật phức tạp thì dùng
useReducer, cần dùng xa thì ghép Reducer + Context. - Khi thật sự chạm trần, mới chọn Zustand (đa số trường hợp) hoặc Redux Toolkit (app lớn, cần dev tools mạnh).
Hãy để nỗi đau thực tế quyết định bước nâng cấp, đừng nâng cấp vì sợ bỏ lỡ. Nắm chắc state của React thuần trước đã, mọi thư viện ngoài đều dễ học hơn khi bạn đã hiểu nền tảng.
Muốn đi theo lộ trình bài bản từ JavaScript tới React tới quản lý state thực chiến, xem Lộ trình học ReactJS.
Học React bài bản, không ghép nhặt rời rạc >Quản lý state là một trong những phần khiến người tự học React mắc kẹt lâu nhất. Trong khóa React PRO của HoleTex, bạn học từ nền tảng state, Context, custom hook cho tới dựng app thật với kiến trúc state gọn gàng, có người chữa bài và review code. Học đúng thứ nhà tuyển dụng cần, không lý thuyết thừa.
Bài liên quan
- React là gì?
- React Hooks là gì? useState và useEffect
- React Router là gì?
- React vs Next.js: khác nhau ở đâu, học cái nào trước?
- Lộ trình học ReactJS từ con số 0 đến đi làm
Nguồn tham khảo: react.dev - Managing State, Passing Data Deeply with Context, Scaling Up with Reducer and Context; Zustand docs; Redux Toolkit; Redux FAQ - When should I use Redux?.