Redux cơ bản

Khi phát triển các ứng dụng JavaScript quy mô vừa và lớn (như React), việc quản lý trạng thái (state) giữa các component thường trở nên phức tạp do hiện tượng prop drilling (truyền dữ liệu qua quá nhiều tầng trung gian). Redux xuất hiện như một giải pháp cứu cánh, đóng vai trò là một kho lưu trữ tập trung duy nhất (Single Source of Truth), giúp dữ liệu của ứng dụng trở nên dễ dự đoán và dễ debug hơn.
3 nguyên tắc cốt lõi của Redux
Để làm chủ Redux, bạn cần nắm vững tư duy thiết kế của nó thông qua 3 nguyên tắc vàng:
- Kho lưu trữ duy nhất (Single Source of Truth): Toàn bộ trạng thái (State) của ứng dụng được lưu trữ trong một mô hình cây (object tree) duy nhất bên trong một Store.
- Trạng thái là chỉ đọc (State is Read-only): Bạn không thể thay đổi trực tiếp
state. Cách duy nhất để làm điều đó là phát đi (dispatch) một Action (một object mô tả sự kiện vừa xảy ra). - Thay đổi bằng hàm thuần túy (Changes are Made with Pure Functions): Để xác định cách cây trạng thái bị thay đổi bởi action, bạn cần viết các Reducer. Reducer phải là pure function – nhận vào state cũ, action và trả về một state mới hoàn toàn mà không làm thay đổi dữ liệu gốc.
Để dễ hình dung, bạn có thể liên tưởng Redux với Ngân hàng, trong đó:
- Kho lưu trữ: như két sắt trung tâm, nơi lưu trữ toàn bộ tiền (State) của ngân hàng.
- Trạng thái chỉ đọc: bạn chỉ có thể xem số dư (State) qua màn hình và không có quyền để mở két sắt và thay đổi số tiền.
- Thay đổi bằng hàm thuần túy: khi cần thay đổi số tiền, bạn phải điền thông tin (Action) để gửi đến Giao dịch viên (Reducer). Giao dịch viên sẽ tuân thủ theo đúng quy trình (Pure function) để cập nhật số dư mới vào hệ thống.
Sơ đồ Luồng dữ liệu một chiều của Redux
Sau khi đã hiểu các khái niệm, hãy cùng xem chúng kết hợp với nhau như thế nào trong thực tế. Redux hoạt động dựa trên một luồng dữ liệu một chiều (Unidirectional Data Flow) nghiêm ngặt. Sơ đồ dưới đây minh họa chính xác cách thức dữ liệu di chuyển trong một ứng dụng Redux, đó là một vòng tuần hoàn khép kín gồm 4 bước chính:
- View (Component): Người dùng tương tác với giao diện (ví dụ: nhấn nút).
- Actions: Giao diện không thay đổi trực tiếp dữ liệu. Thay vào đó, nó “phát đi” (dispatch) một Action (một object mô tả sự kiện vừa xảy ra, kèm theo dữ liệu nếu có).
- Reducers: Action được chuyển đến Reducer. Reducer nhận Action và trạng thái hiện tại (State), sau đó tính toán và trả về một trạng thái mới hoàn toàn mà không làm thay đổi state cũ.
- Store: Trạng thái mới do Reducer trả về sẽ được cập nhật vào Store (kho lưu trữ trung tâm). Vì View đã “đăng ký” (subscribe) theo dõi Store, nó sẽ tự động tải lại (re-render) để hiển thị dữ liệu mới nhất.

Redux Toolkit (RTK) – Hệ sinh thái Redux hiện đại
Trước đây, viết Redux thuần đòi hỏi rất nhiều code rườm rà (boilerplate code). Hiện nay, Redux Toolkit (RTK) đã trở thành tiêu chuẩn chính thức được khuyến nghị bởi Redux team. RTK giúp cấu hình Store nhanh chóng, giảm thiểu code thừa và tích hợp sẵn các công cụ mạnh mẽ như Immer (cho phép viết code “thay đổi trực tiếp” nhưng thực chất là tạo ra state mới) hay Redux Thunk để xử lý bất đồng bộ (gọi API).
Các thành phần cốt lõi tạo nên sức mạnh của Redux Toolkit (RTK)
configureStore(): Thay thế chocreateStoretruyền thống. Tự động cấu hình Redux Store với các thiết lập mặc định tối ưu (như tự động tích hợp Redux DevTools và các middleware mặc định để ngăn chặn lỗi làm thay đổi trực tiếpstate).createSlice(): Công cụ quan trọng nhất của RTK. Giúp bạn tự động tạo cả Actions và Reducers chỉ trong một hàm duy nhất. Bạn chỉ cần định nghĩa name, initialState và các reducer functions.createAsyncThunk(): Giải pháp tích hợp sẵn để xử lý các tác vụ bất đồng bộ (như gọi API). Nó tự động sinh ra các action creators dựa trên 3 trạng thái của Promise:pending,fulfilled, vàrejected.- RTK Query (
createApi): Module nâng cao dành riêng cho việc fetch và cache dữ liệu từ API. RTK Query giúp bạn giảm bớt code thủ công bằng cách tự động quản lý trạng tháiloading,error,datavà hỗ trợ caching, polling dữ liệu thông minh. - Tích hợp sẵn Immer.js: RTK sử dụng thư viện Immer dưới dạng mặc định, cho phép bạn viết code theo phong cách “thay đổi trực tiếp” (mutating code) như
state.value += 1mà vẫn đảm bảo nguyên tắc bất biến (immutability) ở phía sau.
Các khái niệm cốt lõi trong Redux Toolkit
Để làm việc hiệu quả với RTK, bạn chỉ cần nắm vững 4 khái niệm quan trọng gồm: Store, Slice, Actions và Selectors.
Store
Store là kho lưu trữ tập trung toàn bộ state của ứng dụng. Với RTK, Store được khởi tạo đơn giản bằng hàm configureStore(), tự động tích hợp sẵn middleware và Redux DevTools. Hàm này nhận vào một object cấu hình với các thuộc tính chính:
reducer: (Bắt buộc) Object chứa danh sách các slice reducers hoặc một root reducer duy nhất để tổng hợp state của toàn ứng dụng.middleware: (Tùy chọn) Hàm callback dùng để tùy chỉnh danh sách middleware. Mặc định RTK đã tự tích hợpredux-thunkvà middleware kiểm tra lỗi.devTools: (Tùy chọn) Bật/tắt việc kết nối với Redux DevTools Extension trên trình duyệt (mặc định làtrue).
Ví dụ: Cấu hình một Store kết hợp nhiều reducers:
import { configureStore } from "@reduxjs/toolkit"; import counterReducer from "./counterSlice"; import cartReducer from "./cartSlice"; export const store = configureStore({ reducer: { counter: counterReducer, cart: cartReducer, }, // middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), devTools: process.env.NODE_ENV !== "production", // Tắt DevTools ở production });
Slice
Khái niệm trung tâm của RTK. Một slice đại diện cho logic và state của một tính năng cụ thể (ví dụ: counterSlice, cartSlice). Hàm createSlice() sẽ gom nhóm cả Initial State, Reducers và Actions (có thể cả Selectors) vào chung một file duy nhất, giúp dễ tìm kiếm và bảo trì. Hàm này nhận vào một object để định nghĩa một Slice, bao gồm các thuộc tính cốt lõi sau:
name: (Bắt buộc) Chuỗi định danh (string) tên của slice. Chuỗi này sẽ làm tiền tố (prefix) cho các action types tự động sinh ra (ví dụ:'counter').initialState: (Bắt buộc) Trạng thái ban đầu của slice đó (có thể là number, object, array,…).reducers: (Bắt buộc) Object chứa các hàm reducer xử lý các action đồng bộ. Trong mỗi hàm reducer:- Tham số thứ nhất là
state(được bảo vệ/thao tác trực tiếp nhờ Immer.js). - Tham số thứ hai là
action(chứaaction.payloadlà dữ liệu truyền vào).
- Tham số thứ nhất là
extraReducers: (Tùy chọn) Dùng để xử lý các action ngoại vi hoặc bất đồng bộ (như các action được sinh ra từcreateAsyncThunk).
Ví dụ: Tạo một counterSlice quản lý việc tăng/giảm số đếm:
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "./store";
export const counterSlice = createSlice({
name: "counter", // Tên slice
initialState: { value: 0 }, // State ban đầu
reducers: {
// Hàm reducer không tham số payload
increment: (state) => { state.value += 1; }, // Cập nhật trực tiếp nhờ Immer
// Hàm reducer có nhận dữ liệu payload
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
// Trích xuất các hàm action creators để sử dụng
export const { increment, incrementByAmount } = counterSlice.actions;
// Định nghĩa Selector với giá trị cần lấy ngay tại đây và export ra
export const selectCount = (state: RootState) => state.counter.value;
// Export mặc định reducer để truyền vào configureStore
export default counterSlice.reducer;
Actions
Actions là các object chứa thông tin mô tả sự kiện vừa xảy ra (type) và dữ liệu đi kèm (payload).
Với RTK, bạn không cần tự viết các hàm khởi tạo action (action creators) rườm rà vì sau khi gọi createSlice(), RTK sẽ tự động sinh ra object actions tương ứng với tên các hàm reducer mà bạn định nghĩa.
// Ví dụ khi gọi increment() sẽ trả về action object như bên dưới { type: "counter/increment" } // Tương tự gọi incrementByAmount(5) { type: "counter/incrementByAmount", payload: 5 }
Sử dụng useDispatch() – một hook từ react-redux dùng để bắn (dispatch) action creator đó vào Store.
Ví dụ: Lấy các action creators ra và dispatch đi.
import { useDispatch } from "react-redux"; import { increment, incrementByAmount } from "./counterSlice"; // Trong React Component: const dispatch = useDispatch(); // Bắn action không tham số dispatch(increment()); // Bắn action kèm dữ liệu payload (5) dispatch(incrementByAmount(5));
Selectors
Selectors là các hàm nhận vào toàn bộ state của Redux Store và trả về đúng dữ liệu mà Component cần. Điều này giúp Component nhẹ hơn và không lo bị ảnh hưởng hay re-render không cần thiết khi các vùng state khác thay đổi.
Sử dụng useSelector() – một hook từ react-redux nhận vào hàm Selector, lấy dữ liệu từ Store và tự động đăng ký re-render Component mỗi khi dữ liệu đó thay đổi.
💡Típ: Để tối ưu cho việc tái sử dụng và tránh lặp code, bạn có thể định nghĩa các selector trực tiếp bên trong file slice (với slice couter đơn giản như ví dụ trên). Với slice quá lớn và nhiều logic phức tạp, bạn cũng có thể tạo riêng file selector và đặt cùng cấp với file slice để tách biệt rõ ràng giữa logic thay đổi state (reducer) và logic trích xuất dữ liệu (selector). Cuối cùng, nếu không có nhu cầu tái sử dụng và chỉ dùng ở 1 Component cụ thể, bạn có thể tạo selector trực tiếp trong file Component.
Ví dụ: Định nghĩa hàm lấy giá trị value của counter và dùng trong React Component:
import { useSelector } from "react-redux";
// Lấy trực tiếp selector từ file slice
import { selectCount } from "./counterSlice";
// Sử dụng Selector bên trong React Component
const CounterView = () => {
// Lấy chính xác giá trị value từ store
const count = useSelector(selectCount);
return <div>Giá trị hiện tại: {count}</div>;
};
Thực hành
Hãy cùng xây dựng một Ứng dụng đếm số (Counter) đơn giản nhưng chuẩn chỉnh bằng React Redux và Redux Toolkit.
Bước 1: Cài đặt môi trường & thư viện
Tùy thuộc vào trạng thái dự án hiện tại của bạn, hãy chọn câu lệnh cài đặt phù hợp dưới đây:
- Trường hợp 1: Dự án của bạn đã được khởi tạo bằng TypeScript sẵn (Khuyên dùng):
Nếu bạn tạo dự án bằng các template hỗ trợ sẵn TypeScript (như Vite hoặc Next.js), bạn chỉ cần cài đặt Redux Toolkit và React Redux:
# Ví dụ khởi tạo source với Vite npm create vite@latest my-app -- --template react-ts # Cài đặt Redux Toolkit npm install @reduxjs/toolkit react-redux # Cài đặt thêm react-router-dom để điều hướng giao diện (Nếu dùng React.js) npm install react-router-dom
- Trường hợp 2: Dự án của bạn là React JavaScript thuần và muốn nâng cấp lên TypeScript
Hãy chạy câu lệnh gộp dưới đây để cài đặt đồng thời các thư viện Redux cốt lõi cùng môi trường compile và các file định nghĩa kiểu dữ liệu (types) cho React:
# Cài đặt lại môi trường npm install --save-dev typescript @types/react @types/react-dom # Cài đặt Redux Toolkit và Router npm install @reduxjs/toolkit react-redux react-router-dom
Bước 2: Tạo Slice (Quản lý Action và Reducer)
Với RTK, chúng ta sử dụng createSlice để gộp cả định nghĩa Action và Reducer vào một nơi duy nhất. Tạo thư mục slices để chứa các slice theo từng tính năng. Ví dụ tạo folder src/slices/counter để chứa các slices dành cho việc đếm. Để tiện cho việc nâng cấp ví dụ sau này, chúng ta sẽ đặt tên file slice là basicCounterSlice.ts.
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
// Định nghĩa kiểu dữ liệu cho state của slice này
interface CounterState {
value: number;
}
// Khởi tạo state ban đầu với kiểu dữ liệu đã định nghĩa
const initialState: CounterState = {
value: 0,
};
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// Sử dụng PayloadAction<number> để chỉ định payload bắt buộc phải là kiểu số
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
Bước 3: Cấu hình Redux Store
Chúng ta sẽ gom tất cả các reducer lại để tạo thành một Store trung tâm. Tương tự tạo file src/stores/basicStore.ts để dễ nâng cấp bài học sau này. Sử dụng configureStore để cấu hình Store.
import { configureStore } from "@reduxjs/toolkit"; import counterReducer from "../slices/counter/basicCounterSlice"; export const basicStore = configureStore({ reducer: { counter: counterReducer, }, }); // Xuất các kiểu dữ liệu RootState và AppDispatch từ chính store export type BasicRootState = ReturnType<typeof basicStore.getState>; export type BasicAppDispatch = typeof basicStore.dispatch;
Bạn có thể tạo thêm một folder src/hooks để định nghĩa trước các Hooks đã được ép kiểu sẵn (Typed Hooks). Điều này giúp khi viết code ở component, bạn không cần phải import RootState hay AppDispatch lặp lại nhiều lần ở nhiều nơi cần sử dụng, giảm việc lặp code. Tương tự chúng ta sẽ tạo basicHooks.ts để sử dụng trong ví dụ cơ bản này.
import { useDispatch, useSelector } from "react-redux";
import type { BasicRootState, BasicAppDispatch } from "../stores/basicStore";
// Sử dụng xuyên suốt ứng dụng thay vì useDispatch và useSelector thông thường
export const useBasicAppDispatch = () => useDispatch<BasicAppDispatch>();
export const useBasicAppSelector = <TSelected>(
selector: (state: BasicRootState) => TSelected
) => useSelector(selector);
Bước 4: Liên kết Store và sử dụng Redux Hooks trong Component
Chúng ta sẽ tạo folder src/app/ để chứa trực tiếp các trang giao diện. Tại mỗi file trang này, chúng ta sẽ định nghĩa một Component giao diện chính và một Component Demo được bao bọc (wrap) bởi <Provider> của react-redux để liên kết cô lập với Store tương ứng.
Trong component con, chúng ta dùng useSelector để lấy dữ liệu từ store và useDispatch để gửi action đi. Vì chúng ta đã tạo sẵn 2 hooks này ở file src/hooks/basicHooks.ts trước đó, nên chỉ cần import vào file src/app/CounterPage.tsx để sử dụng.
import React, { useState } from "react"; import { Provider } from "react-redux"; import { basicStore } from "../stores/basicStore"; import { useBasicAppSelector, useBasicAppDispatch } from "../hooks/basicHooks"; import { increment, decrement, incrementByAmount, } from "../slices/counter/basicCounterSlice"; function CounterDemo() { const count = useBasicAppSelector((state) => state.counter.value); const dispatch = useBasicAppDispatch(); // Thêm state local để quản lý giá trị ô nhập const [incrementAmount, setIncrementAmount] = useState("2"); const value = Number(incrementAmount) || 0; return ( <div style={{ textAlign: "center", marginTop: "50px" }}> <h2>Ví dụ cơ bản (Đồng bộ)</h2> <div style={{ fontSize: "3rem", margin: "20px", fontWeight: "bold" }}> {count} </div> {/* Các nút bấm cơ bản */} <button onClick={() => dispatch(increment())}>Tăng 1</button> <button onClick={() => dispatch(decrement())} style={{ marginLeft: "10px" }} > Giảm 1 </button> {/* Khu vực nhập số lượng nâng cao của bài 1 */} <div style={{ marginTop: "20px" }}> <input type="number" value={incrementAmount} onChange={(e) => setIncrementAmount(e.target.value)} style={{ width: "60px", padding: "5px", textAlign: "center" }} /> <button onClick={() => dispatch(incrementByAmount(value))} style={{ marginLeft: "10px" }} > Tăng theo số lượng </button> </div> </div> ); } export default function CounterPage() { return ( <Provider store={basicStore}> <CounterDemo /> </Provider> ); }
Bước 5: Thiết lập Router
Để có thể dễ dàng chuyển đổi và trải nghiệm song song cả hai ví dụ (Cơ bản & Nâng cao) ngay trên giao diện ứng dụng, chúng ta sẽ kích hoạt React Router bằng cách bọc BrowserRouter tại file root, sau đó thiết lập menu điều hướng nhanh.
import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import App from "./App"; const rootElement = document.getElementById("root")!; const root = ReactDOM.createRoot(rootElement); root.render( <React.StrictMode> <BrowserRouter> <App /> </BrowserRouter> </React.StrictMode> );
Tại App.tsx chúng ta sẽ tạo một trang giao diện với menu điều hướng đến 2 trang ví dụ cơ bản và nâng cao (dành cho bài sau).
import React from "react"; import { Routes, Route, Link } from "react-router-dom"; import CounterPage from "./app/CounterPage"; import AdvancedPage from "./app/AdvancedPage"; function App() { return ( <div> {/* Thanh Menu điều hướng nhanh */} <nav style={{ padding: "15px", background: "#23282d", textAlign: "center" }} > <Link to="/" style={{ color: "#fff", marginRight: "20px", textDecoration: "none", fontWeight: "bold", }} > Redux cơ bản </Link> <Link to="/advanced" style={{ color: "#fff", textDecoration: "none", fontWeight: "bold" }} > Redux nâng cao (Thunk & RTK Query) </Link> </nav> {/* Định tuyến trực tiếp đến các trang trong folder app */} <Routes> <Route path="/" element={<CounterPage />} /> <Route path="/advanced" element={<AdvancedPage />} /> </Routes> </div> ); } export default App;
Kết quả
Như vậy chúng ta đã vừa hoàn thành xong việc tạo một ứng dụng Counter cơ bản với cấu trúc source code cơ bản và dễ nâng cấp. Bạn có thể tham khảo source code tại đây.
Khi nào nên sử dụng Redux?
Mặc dù rất mạnh mẽ, Redux không phải là “viên đạn bạc” cho mọi dự án. Bạn cần cân nhắc kỹ trước khi đưa vào ứng dụng:
- NÊN dùng khi:
- Ứng dụng có lượng trạng thái lớn, được cập nhật liên tục bởi nhiều phần khác nhau trong giao diện.
- Các component nằm ở các nhánh/tầng rất xa nhau trong cây DOM cần truy cập và chỉnh sửa chung một nguồn dữ liệu.
- Bạn cần các tính năng nâng cao như Time-travel debugging (du hành thời gian để debug với Redux DevTools) hay tính năng Undo/Redo.
- KHÔNG NÊN dùng khi:
- Dự án chỉ ở quy mô nhỏ hoặc vừa, ít dữ liệu dùng chung.
- Bạn hoàn toàn có thể giải quyết bài toán bằng
useState,useReducerkết hợp vớiContext APIcủa React.
Các tính năng mở rộng mạnh mẽ khác của Redux Toolkit
Sau khi đã làm chủ luồng dữ liệu cơ bản, bạn có thể khám phá thêm các công cụ nâng cao mà Redux Toolkit (RTK) tích hợp sẵn để giải quyết các bài toán thực tế trong các ứng dụng lớn:
- RTK Query: Công cụ quản lý và gọi API cực mạnh. Thay vì phải tự viết các hàm bất đồng bộ và quản lý trạng thái
loading,error,data, RTK Query tự động sinh ra các React Hooks giúp bạn fetch dữ liệu, tự động lưu bộ nhớ đệm (caching) và tối ưu hiệu năng. - createAsyncThunk: Giải pháp truyền thống giúp bạn xử lý các hành động bất đồng bộ (như gọi API) và tự động quản lý luồng dữ liệu qua 3 trạng thái của một Promise: đang xử lý (
pending), thành công (fulfilled), và thất bại (rejected). - Tích hợp sẵn DevTools & Middleware: RTK tự động kích hoạt Redux DevTools giúp bạn “du hành thời gian” để debug, đồng thời đi kèm các bộ lọc mặc định để ngăn chặn việc sửa đổi trực tiếp state (mutation) sai quy định.
Lời kết
Redux (đặc biệt là Redux Toolkit) giúp việc quản lý state trong ứng dụng JavaScript trở nên minh bạch và có tính kiến trúc rõ ràng. Bằng cách tuân thủ luồng dữ liệu một chiều (Unidirectional Data Flow) nghiêm ngặt, bạn sẽ giảm thiểu tối đa các lỗi bất ngờ (side effects) và giúp dự án dễ dàng mở rộng trong tương lai.
💡 Trong bài viết tiếp theo, chúng ta sẽ cùng nhau thực hành cách gọi API và xử lý dữ liệu bất đồng bộ với RTK Query và createAsyncThunk nhé!





