Xin chào các bạn quay trở lại với series “React Roadmap – Lộ trình học React 2025“! Chúng ta đã cùng nhau đi qua nhiều chặng đường, từ những khái niệm cơ bản nhất như React là gì, JSX, Props vs State, đến Functional Components với Hooks như useState và useEffect, Custom Hooks, và cả useReducer hay useContext để quản lý state. Hôm nay, chúng ta sẽ lặn sâu vào một chủ đề cực kỳ quan trọng khi phát triển các ứng dụng React lớn: Quản lý State Tập Trung với Redux!
Nếu bạn đã từng làm việc với các ứng dụng React phức tạp, bạn sẽ hiểu rõ những thách thức khi quản lý state ở nhiều component khác nhau. Props drilling (truyền props qua nhiều tầng component) trở nên khó khăn, việc chia sẻ state giữa các component không có quan hệ cha-con trực tiếp cũng không hề dễ dàng. Đây chính là lúc các thư viện quản lý state tập trung như Redux phát huy sức mạnh.
Tuy nhiên, Redux “truyền thống” đôi khi bị than phiền là có quá nhiều boilerplate code, cấu hình phức tạp, đặc biệt với các bạn mới bắt đầu. May mắn thay, cộng đồng Redux đã cho ra đời một giải pháp hiện đại, đơn giản và hiệu quả hơn rất nhiều: Redux Toolkit (RTK).
Bài viết này sẽ là kim chỉ nam giúp bạn “Getting Started” với Redux Toolkit, biến Redux từ một thứ phức tạp thành một công cụ dễ dàng làm chủ. Chúng ta sẽ tìm hiểu RTK là gì, tại sao nó ra đời, các khái niệm cốt lõi và cách tích hợp nó vào ứng dụng React của bạn.
Mục lục
Vì Sao Lại Là Redux Toolkit? Những Vấn Đề Của Redux Truyền Thống
Trước khi nói về RTK, hãy điểm qua một chút về những thách thức của Redux “thuần” (vanilla Redux) mà RTK được tạo ra để giải quyết:
- Quá nhiều Boilerplate: Để thiết lập một state đơn giản, bạn cần tạo hằng số cho action types, action creators, reducers với cấu trúc switch case phức tạp, và cấu hình store. Điều này tốn thời gian và dễ gây lỗi gõ nhầm (typo).
- Yêu cầu Thêm Middleware: Để xử lý các tác vụ bất đồng bộ (async logic) như gọi API, bạn cần tích hợp middleware như Redux Thunk hoặc Redux Saga và cấu hình chúng.
- Quản lý Immutability Thủ Công: Redux yêu cầu bạn không được thay đổi trực tiếp state hiện tại mà phải luôn trả về một state mới. Điều này đòi hỏi code sao chép (copy) state cẩn thận, dễ sai sót nếu không quen thuộc với spread operator hoặc các thư viện hỗ trợ immutability.
- Cấu Hình Store Phức Tạp: Thiết lập store ban đầu yêu cầu kết hợp các reducer, áp dụng middleware, cấu hình Redux DevTools extension… Nếu không có kiến thức vững, bước này có thể khá rắc rối.
Redux Toolkit ra đời với mục tiêu trở thành bộ công cụ tiêu chuẩn, được khuyến nghị để viết logic Redux. Nó gói gọn những practices tốt nhất, giảm thiểu boilerplate và cung cấp sẵn các công cụ cần thiết, giúp trải nghiệm phát triển với Redux trở nên mượt mà và hiệu quả hơn rất nhiều.
Redux Toolkit Là Gì?
Redux Toolkit (RTK) là tập hợp các công cụ và hàm utility giúp đơn giản hóa việc phát triển với Redux. Nó được xem là cách chính thức (official, opinionated) để viết code Redux hiện đại. RTK bao gồm các gói thư viện và các hàm API được thiết kế để giải quyết trực tiếp những vấn đề của Redux truyền thống.
Các thành phần chính mà RTK cung cấp bao gồm:
configureStore()
: Hàm thay thế chocreateStore
của Redux truyền thống, giúp cấu hình store một cách đơn giản, bao gồm sẵn Redux DevTools extension và Redux Thunk middleware.createSlice()
: Một hàm mạnh mẽ giúp định nghĩa reducer, action types, và action creators trong cùng một nơi. Nó tự động tạo ra các action creators và xử lý logic immutable state nhờ tích hợp thư viện Immer.createAsyncThunk()
: Hàm utility chuẩn hóa việc thực hiện các logic bất đồng bộ và dispatch các action dựa trên vòng đời của promise (pending, fulfilled, rejected).createEntityAdapter()
: (Không đi sâu trong bài này, nhưng là một công cụ hữu ích) Giúp quản lý dữ liệu quan hệ/chuẩn hóa (normalized data) trong state.createSelector()
từ thư viện Reselect: (Thường dùng kèm) Giúp tạo các memoized selector hiệu quả để trích xuất dữ liệu từ state.
Nói cách khác, RTK giống như một lớp abstraction trên Redux cốt lõi, cung cấp “pin kèm theo” (batteries-included) để bạn bắt đầu nhanh chóng và viết code Redux tốt hơn ngay từ đầu.
Các Khái Niệm Cốt Lõi Với Redux Toolkit
Mặc dù RTK đơn giản hóa nhiều thứ, các khái niệm cốt lõi của Redux vẫn tồn tại nhưng được thể hiện theo cách mới:
Store
Đây là “ngôi nhà” chứa toàn bộ global state của ứng dụng. Với RTK, bạn tạo store bằng configureStore
:
import { configureStore } from '@reduxjs/toolkit';
// Import các slice reducers của bạn
import counterReducer from './features/counter/counterSlice';
import userReducer from './features/user/userSlice';
export const store = configureStore({
reducer: {
// Các slice reducers được kết hợp lại ở đây
counter: counterReducer,
user: userReducer,
},
// configureStore tự động thêm redux-thunk và bật Redux DevTools extension
// middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(myCustomMiddleware), // Thêm middleware khác nếu cần
// devTools: process.env.NODE_ENV !== 'production', // Bật/tắt DevTools theo môi trường
});
// Định nghĩa kiểu cho RootState và AppDispatch (cho TypeScript)
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
configureStore
tự động thiết lập root reducer bằng cách kết hợp các slice reducers bạn cung cấp, thêm sẵn Redux Thunk làm middleware để xử lý async, và tích hợp Redux DevTools extension (nếu cài đặt trên trình duyệt) mà không cần cấu hình gì thêm.
Slice
Đây là khái niệm trung tâm của RTK. Một “slice” state thường tương ứng với một phần logic hoặc tính năng cụ thể của ứng dụng (ví dụ: user slice, order slice, counter slice). createSlice
cho phép bạn định nghĩa tất cả mọi thứ liên quan đến phần state đó trong cùng một object:
name
: Tên của slice (sẽ dùng làm prefix cho action types).initialState
: Trạng thái ban đầu của slice.reducers
: Object chứa các “reducer functions” để xử lý synchronous logic. Điều kỳ diệu là bên trong này, bạn có thể viết code “mutation” (thay đổi trực tiếp state) nhờ thư viện Immer, nhưng thực tế Immer sẽ tạo ra một state mới dựa trên thay đổi của bạn.extraReducers
: (Tùy chọn) Object hoặc builder function để xử lý các action types không được tạo bởi slice này (ví dụ: action types từcreateAsyncThunk
hoặc từ slice khác).
Ví dụ về một counter slice:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// Định nghĩa kiểu cho state của slice (cho TypeScript)
interface CounterState {
value: number;
}
// Định nghĩa trạng thái ban đầu
const initialState: CounterState = {
value: 0,
};
// Tạo slice sử dụng createSlice
export const counterSlice = createSlice({
name: 'counter', // Tên slice
initialState, // Trạng thái ban đầu
reducers: {
// Các reducer functions xử lý logic đồng bộ
increment: (state) => {
// Immer cho phép chúng ta "mutate" state trực tiếp
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
// Thêm các reducers khác nếu cần
},
// extraReducers: (builder) => { ... } // Dùng để xử lý async actions
});
// createSlice tự động tạo action creators dựa trên tên của các reducer
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// Xuất reducer cho store
export default counterSlice.reducer;
Như bạn thấy, code gọn gàng hơn rất nhiều so với Redux truyền thống! Không còn hằng số action types, không còn switch case lớn.
Actions
Actions là các object JavaScript đơn giản mô tả điều gì đó đã xảy ra trong ứng dụng. Với RTK, các action creators được tự động tạo ra bởi createSlice
dựa trên tên của các reducer functions bạn định nghĩa trong object reducers
. Bạn chỉ việc gọi chúng:
import { increment, decrement } from './counterSlice';
// Dispatch action
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(incrementByAmount(5)); // Với payload
Selectors
Selectors là các hàm giúp trích xuất dữ liệu cụ thể từ state của store. Chúng giúp component chỉ cần biết cách lấy dữ liệu từ state tổng thể mà không cần quan tâm đến cấu trúc chi tiết của state.
// Trong counterSlice.js hoặc một file selectors.js riêng
import { RootState } from '../store'; // Giả sử store ở thư mục gốc
export const selectCount = (state: RootState) => state.counter.value;
Bạn sẽ sử dụng selector này trong component React.
Tích Hợp Redux Toolkit Vào React App
Để sử dụng Redux Toolkit trong ứng dụng React, bạn cần thư viện react-redux
. Nó cung cấp các Hooks để kết nối React components với Redux store.
-
Cài đặt các gói cần thiết:
npm install @reduxjs/toolkit react-redux
-
Tạo Store: (Như ví dụ
configureStore
ở trên). Lưu file này ở đâu đó dễ truy cập, ví dụsrc/app/store.js
. -
Tạo Slice: (Như ví dụ
createSlice
ở trên). Lưu mỗi slice trong một file riêng, ví dụsrc/features/counter/counterSlice.js
. -
Kết nối Store với React App: Sử dụng component
<Provider>
từreact-redux
để bọc quanh root component của ứng dụng (thường là<App>
hoặc<Index>
). Điều này giúp store có thể truy cập được từ mọi component con. Filesrc/index.js
(hoặcsrc/main.jsx
trong React 18+):import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import { store } from './app/store'; import { Provider } from 'react-redux'; ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, );
-
Sử dụng State và Dispatch Actions trong Component: Sử dụng các Hooks
useSelector
vàuseDispatch
từreact-redux
trong các functional components của bạn. Đây là một sự chuyển đổi lớn so với cách kết nối component Redux truyền thống bằngconnect
HOC. Bạn có thể xem lại bài viết về Hooks để hiểu rõ hơn về cách hoạt động của Hooks trong React.import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { decrement, increment, incrementByAmount, selectCount } from './counterSlice'; import type { RootState, AppDispatch } from '../../app/store'; // Import kiểu cho TypeScript function Counter() { // Sử dụng useSelector để lấy state từ store. Nhớ sử dụng selector! const count = useSelector(selectCount); // selectCount được định nghĩa trong counterSlice.js // Hoặc nếu không dùng selector riêng: // const count = useSelector((state: RootState) => state.counter.value); // Sử dụng useDispatch để lấy hàm dispatch const dispatch = useDispatch<AppDispatch>(); // Thêm kiểu AppDispatch cho TypeScript return ( <div> <div> <button aria-label="Decrement value" onClick={() => dispatch(decrement())} > Decrement </button> <span>{count}</span> <button aria-label="Increment value" onClick={() => dispatch(increment())} > Increment </button> </div> <div> <input type="number" defaultValue="2" onChange={(e) => { const amount = Number(e.target.value) || 0; // Dispatch action với payload dispatch(incrementByAmount(amount)); }} /> </div> </div> ); } export default Counter;
useSelector
nhận một hàm selector làm đối số, hàm này sẽ nhận toàn bộ Redux state làm đầu vào và trả về dữ liệu mà component cần. Khi dữ liệu này thay đổi, component sẽ tự động re-render.useDispatch
trả về hàmdispatch
từ Redux store, cho phép bạn gửi (dispatch) các action để cập nhật state.
Xử Lý Logic Bất Đồng Bộ với createAsyncThunk
Một trong những trường hợp phổ biến nhất trong ứng dụng web là gọi API để lấy hoặc gửi dữ liệu. Các thao tác này là bất đồng bộ (async). Redux Toolkit cung cấp createAsyncThunk
để chuẩn hóa cách xử lý này.
createAsyncThunk
nhận một action type string (ví dụ: `’users/fetchById’`) và một async function (gọi là “payload creator”). Payload creator này sẽ thực hiện logic bất đồng bộ của bạn (ví dụ: gọi API) và trả về một Promise. createAsyncThunk
sẽ tự động dispatch các action types theo vòng đời của Promise:
pending
: Promise đang được thực thi.fulfilled
: Promise được giải quyết thành công (resolve).rejected
: Promise bị từ chối (reject).
Bạn sẽ xử lý các action types này trong phần extraReducers
của slice.
Ví dụ về slice xử lý việc lấy dữ liệu người dùng bất đồng bộ:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { fetchUserByIdAPI } from './api'; // Giả sử bạn có một API function
interface User {
id: string;
name: string;
// ... các thuộc tính khác
}
interface UserState {
entities: User[];
loading: 'idle' | 'pending' | 'succeeded' | 'failed';
error: string | null;
}
const initialState: UserState = {
entities: [],
loading: 'idle',
error: null,
};
// Tạo async thunk
export const fetchUserById = createAsyncThunk(
'users/fetchById', // Action type string
// Payload creator - hàm async
async (userId: string, { rejectWithValue }) => {
try {
const user = await fetchUserByIdAPI(userId);
return user; // Giá trị trả về sẽ là payload của action 'fulfilled'
} catch (error: any) {
// Sử dụng rejectWithValue để trả về lỗi cụ thể
return rejectWithValue(error.message);
}
}
);
const userSlice = createSlice({
name: 'users',
initialState,
reducers: {
// Reducers đồng bộ khác (nếu có)
},
extraReducers: (builder) => {
builder
// Xử lý action 'pending'
.addCase(fetchUserById.pending, (state) => {
state.loading = 'pending';
state.error = null; // Reset lỗi khi bắt đầu loading mới
})
// Xử lý action 'fulfilled'
.addCase(fetchUserById.fulfilled, (state, action) => {
state.loading = 'succeeded';
// Thêm user vừa fetch vào state (ví dụ)
state.entities.push(action.payload);
})
// Xử lý action 'rejected'
.addCase(fetchUserById.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.payload as string; // Payload chứa lỗi từ rejectWithValue
});
},
});
export default userSlice.reducer;
Và cách sử dụng trong component:
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUserById } from './userSlice';
import type { RootState, AppDispatch } from '../../app/store';
function UserProfile({ userId }: { userId: string }) {
const dispatch = useDispatch<AppDispatch>();
const user = useSelector((state: RootState) =>
state.users.entities.find(u => u.id === userId)
);
const loading = useSelector((state: RootState) => state.users.loading);
const error = useSelector((state: RootState) => state.users.error);
useEffect(() => {
// Dispatch async thunk trong useEffect
dispatch(fetchUserById(userId));
}, [userId, dispatch]); // Dependencies
if (loading === 'pending') {
return <div>Loading...</div>;
}
if (loading === 'failed') {
return <div>Error: {error}</div>;
}
if (!user) {
return <div>User not found.</div>;
}
return (
<div>
<h2>User Profile</h2>
<p>Name: {user.name}</p>
{/* ... các thông tin khác */}
</div>
);
}
export default UserProfile;
createAsyncThunk
giúp chúng ta quản lý trạng thái của các cuộc gọi API (đang loading, thành công, thất bại) một cách nhất quán và rõ ràng hơn rất nhiều.
Lợi Ích Khi Sử Dụng Redux Toolkit
Tổng kết lại, việc chuyển sang sử dụng Redux Toolkit mang lại nhiều lợi ích đáng kể:
- Giảm Boilerplate: Code ít lặp lại hơn, dễ đọc và dễ viết hơn.
- Đơn Giản Hóa Cấu Hình: Store setup trở nên đơn giản với
configureStore
. - Tích Hợp Sẵn Middleware & DevTools: Không cần cài đặt hay cấu hình Redux Thunk và Redux DevTools thủ công.
- Quản Lý Immutability Dễ Dàng: Nhờ Immer, bạn có thể viết code reducer “mutation” logic nhưng vẫn đảm bảo immutability.
- Xử Lý Async Chuẩn Hóa:
createAsyncThunk
cung cấp cách tiếp cận nhất quán để xử lý các tác vụ bất đồng bộ. - Khuyến Khích Best Practices: RTK được thiết kế để dẫn dắt bạn viết code Redux theo các mẫu khuyến nghị.
Redux Toolkit thực sự đã làm cho Redux trở nên dễ tiếp cận và sử dụng hơn rất nhiều, đặc biệt với các bạn mới bắt đầu hoặc những ai cảm thấy ngợp trước sự phức tạp của Redux truyền thống.
Bảng So Sánh: Redux Truyền Thống vs. Redux Toolkit
Để có cái nhìn rõ ràng hơn, hãy xem bảng so sánh này:
Đặc Điểm | Redux Truyền Thống | Redux Toolkit |
---|---|---|
Cấu Hình Store | Cần kết hợp createStore , applyMiddleware , composeWithDevTools thủ công. |
Sử dụng configureStore đơn giản, tích hợp sẵn Middleware và DevTools. |
Định Nghĩa Reducers & Actions | Tạo hằng số action types, action creators riêng, reducers dùng switch case lớn. | Sử dụng createSlice để định nghĩa tất cả trong một object duy nhất. Tự động tạo action creators. |
Xử Lý Immutability | Phải tự đảm bảo immutability (sử dụng spread operator, Object.assign, hoặc thư viện ngoài). | Tích hợp Immer, cho phép viết code mutation logic bên trong reducers đồng bộ. |
Xử Lý Async Logic | Cần cài đặt và cấu hình middleware (Redux Thunk, Redux Saga). | createAsyncThunk được tích hợp sẵn, chuẩn hóa quy trình xử lý async. |
Boilerplate Code | Nhiều boilerplate, đặc biệt khi thêm một tính năng mới. | Giảm đáng kể boilerplate, giúp viết code nhanh hơn và ít lỗi hơn. |
Mức Độ Khuyến Nghị | Vẫn hoạt động nhưng không còn là cách tiếp cận khuyến nghị cho dự án mới. | Là cách chính thức, được khuyến nghị và là tiêu chuẩn hiện đại. |
Bảng trên cho thấy rõ ràng sự vượt trội và tiện lợi của Redux Toolkit so với cách tiếp cận cũ.
Giới thiệu nhanh về RTK Query
Trước khi kết thúc, không thể không nhắc đến RTK Query, một phần cực kỳ mạnh mẽ của Redux Toolkit dành riêng cho việc quản lý data fetching và caching.
Nếu createAsyncThunk
là công cụ “thấp cấp” hơn cho async logic nói chung, thì RTK Query là một layer “cao cấp” được xây dựng trên RTK, chuyên sâu vào việc:
- Tự động quản lý trạng thái loading, error.
- Tự động cache dữ liệu.
- Tự động re-fetch (lấy lại dữ liệu).
- Tự động cập nhật cache khi dữ liệu thay đổi (mutation).
- Giảm đáng kể lượng code bạn cần viết để quản lý data fetching so với việc tự viết thunks.
Mặc dù bài viết này tập trung vào các khái niệm cốt lõi của RTK, hãy ghi nhớ RTK Query là một công cụ bạn chắc chắn nên khám phá tiếp sau khi đã làm quen với `createSlice` và `createAsyncThunk`. Nó sẽ giúp bạn giải quyết các bài toán quản lý dữ liệu từ server một cách vô cùng hiệu quả.
Kết Luận
Việc quản lý state trong các ứng dụng React phức tạp là một kỹ năng quan trọng. Redux, với sự hỗ trợ đắc lực từ Redux Toolkit, cung cấp một giải pháp mạnh mẽ, dễ bảo trì và dễ mở rộng.
Redux Toolkit không chỉ giúp bạn viết code Redux nhanh hơn, ít lỗi hơn mà còn hướng dẫn bạn đi theo các best practices được cộng đồng khuyến nghị. Nếu bạn đã từng “ngại” Redux vì sự phức tạp của nó, thì bây giờ là lúc hoàn hảo để thử lại với Redux Toolkit.
Hãy bắt đầu bằng cách cài đặt RTK vào dự án React của bạn, tạo slice đầu tiên, kết nối nó với component và trải nghiệm sự khác biệt. Thực hành là chìa khóa để làm chủ bất kỳ công nghệ nào.
Chúng ta đã khám phá một công cụ cực kỳ hữu ích trên React Roadmap. State management là một chặng đường dài, và Redux Toolkit sẽ là người bạn đồng hành đáng tin cậy của bạn trên hành trình đó.
Hẹn gặp lại các bạn trong các bài viết tiếp theo của series “React Roadmap”!