Redux nâng cao – Làm chủ createAsyncThunk và RTK Query

Trong bài viết trước, chúng ta đã cùng nhau xây dựng một ứng dụng Counter cơ bản để hiểu luồng dữ liệu đồng bộ của Redux Toolkit. Tuy nhiên, trong các dự án thực tế, phần lớn thời gian chúng ta phải làm việc với các tác vụ bất đồng bộ (Asynchronous) như gọi API lấy dữ liệu từ Server.
Để giải quyết bài toán này, Redux Toolkit cung cấp hai vũ khí hạng nặng: createAsyncThunk và RTK Query. Bài viết này sẽ giúp bạn làm chủ cả hai công cụ này thông qua việc mở rộng dự án Counter sẵn có nhưng với một cấu trúc thư mục hoàn toàn tách biệt để giữ nguyên vẹn mã nguồn của bài học trước.
Thực hành
Thiết kế Cấu trúc thư mục nâng cao
Để đảm bảo không làm thay đổi hoặc mất đi phần mã nguồn của bài viết cơ bản, chúng ta sẽ tạo thêm các thư mục và file mới nằm song song trong /src như sau:
src/├── components/ # Các component giao diện dùng chung├── hooks/ # Nơi chứa các Typed Hooks đã ép kiểu sẵn│ ├── basicHooks.ts│ └── advancedHooks.ts├── slices/ # Logic Actions & Reducers│ └── counter/│ ├── basicCounterSlice.ts│ ├── advancedCounterSlice.ts│ └── postsApi.ts├── stores/ # Cấu hình kho lưu trữ tập trung│ ├── basicStore.ts│ └── advancedStore.ts├── app/ # Chứa trực tiếp các trang giao diện hiển thị│ ├── CounterPage.tsx # Trang demo của bài cơ bản (Bọc basicStore)│ └── AdvancedPage.tsx # Trang demo của bài nâng cao (Bọc advancedStore)├── App.tsx # Router chính và thanh điều hướng (Navbar)└── index.tsx # File root khởi chạy ứng dụng (BrowserRouter)
createAsyncThunk – xử lý bất đồng bộ kiểu “Truyền thống”
Trước đây, Redux Thunk được sử dụng như một phần mềm trung gian giúp Redux xử lý các logic bất đồng bộ, bạn phải tự viết thủ công từng trạng thái loading, success, error và tự gọi dispatch cho từng cái khiến code bị dài dòng, lặp lại. Còn với hiện tại, Redux Toolkit (RTK) đã tích hợp sẵn Thunk thông qua hàm hỗ trợ createAsyncThunk. Đây là công cụ giúp bạn tự tay kiểm soát luồng đi của một hàm bất đồng bộ, giúp code ngắn gọn và chuẩn hóa cách gọi API trong Redux. Nó tự động sinh ra các action tương ứng với 3 trạng thái của một Promise:
pending: Đang gọi API.fulfilled: Gọi API thành công và có dữ liệu trả về.rejected: Gọi API thất bại (lỗi mạng, server sập…).
Chúng ta sẽ thực hành với tính năng Tự động tăng số đếm sau 1 giây (Giả lập API). Hãy tạo một file Slice nâng cao mới tại src/slices/counter/advancedCounterSlice.ts
import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit";
interface AdvancedCounterState {
value: number;
status: "idle" | "loading" | "failed";
}
const initialState: AdvancedCounterState = {
value: 0,
status: "idle",
};
// Hàm bất đồng bộ giả lập delay 1 giây giống như đợi Server phản hồi
export const incrementAsync = createAsyncThunk(
"advancedCounter/fetchCount",
async (amount: number) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return amount;
}
);
export const advancedSlice = createSlice({
name: "advancedCounter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
},
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = "loading";
})
.addCase(
incrementAsync.fulfilled,
(state, action: PayloadAction<number>) => {
state.status = "idle";
state.value += action.payload;
}
)
.addCase(incrementAsync.rejected, (state) => {
state.status = "failed";
});
},
});
export const { increment, decrement } = advancedSlice.actions;
export default advancedSlice.reducer;
RTK Query – Kẻ hủy diệt Boilerplate Code
Nếu như createAsyncThunk bắt bạn phải tự quản lý trạng thái loading, error và viết khá nhiều dòng code tại extraReducers, thì RTK Query sinh ra để giải phóng bạn khỏi những việc đó.
RTK Query hoạt động dựa trên cơ chế khai báo (declarative). Bạn chỉ cần định nghĩa các endpoints, nó sẽ tự động tạo ra các React Hooks chứa sẵn mọi trạng thái dữ liệu mong muốn.
Chúng ta sẽ thực hành Lấy dữ liệu danh sách bài viết từ API thật. Hãy tạo file API Service riêng tại thư mục: src/.slices/counter/postsApi.ts
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const postsApi = createApi({
reducerPath: "postsApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://jsonplaceholder.typicode.com/",
}),
endpoints: (builder) => ({
// Định nghĩa endpoint lấy danh sách bài viết giới hạn 3 phần tử
getPosts: builder.query<any[], void>({
query: () => "posts?_limit=3",
}),
}),
});
// RTK Query tự động sinh ra hook này dựa trên tên endpoint getPosts ở trên
export const { useGetPostsQuery } = postsApi;
Cấu hình Store nâng cao
Để gom nhóm các reducers nâng cao lại với nhau, chúng ta tạo một file cấu hình Store nâng cao tại src/stores/advancedStore.ts.
import { configureStore } from "@reduxjs/toolkit";
import advancedCounterReducer from "../slices/counter/advancedCounterSlice";
import { postsApi } from "../slices/counter/postsApi";
export const advancedStore = configureStore({
reducer: {
// Sử dụng reducer nâng cao mới
advancedCounter: advancedCounterReducer,
[postsApi.reducerPath]: postsApi.reducer,
},
// Bắt buộc thêm middleware của RTK Query để kích hoạt tính năng caching
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(postsApi.middleware),
});
export type AdvancedRootState = ReturnType<typeof advancedStore.getState>;
export type AdvancedAppDispatch = typeof advancedStore.dispatch;
Tiếp đến, chúng ta sẽ tạo advancedHooks.ts để định nghĩa trước các hook cần sử dụng trong ví dụ nâng cao này:
import { useDispatch, useSelector } from "react-redux";
import type {
AdvancedRootState,
AdvancedAppDispatch,
} from "../stores/advancedStore";
export const useAdvancedAppDispatch = () => useDispatch<AdvancedAppDispatch>();
export const useAdvancedAppSelector = <TSelected>(
selector: (state: AdvancedRootState) => TSelected
) => useSelector(selector);
Tạo trang giao diện Nâng cao
Thay vì ghi đè trực tiếp lên file giao diện cũ, chúng ta sẽ tạo riêng một file src/app/AdvancedPage.tsx chứa trang giao diện nâng cao, sau đó dùng <Provider> bọc cô lập để liên kết riêng với advancedStore.
import React from "react"; import { Provider } from "react-redux"; import { advancedStore } from "../stores/advancedStore"; import { useAdvancedAppSelector, useAdvancedAppDispatch, } from "../hooks/advancedHooks"; import { increment, decrement, incrementAsync, } from "../slices/counter/advancedCounterSlice"; import { useGetPostsQuery } from "../slices/counter/postsApi"; function AdvancedDemo() { const count = useAdvancedAppSelector((state) => state.advancedCounter.value); const status = useAdvancedAppSelector( (state) => state.advancedCounter.status ); const dispatch = useAdvancedAppDispatch(); // Gọi API bằng RTK Query const { data: posts, isLoading, isError } = useGetPostsQuery(); return ( <div style={{ textAlign: "center", marginTop: "50px" }}> <h2>Ví dụ nâng cao (Thunk & RTK Query)</h2> <div style={{ fontSize: "2rem", margin: "20px" }}>{count}</div> {/* 1. Tính năng đếm đồng bộ */} <button onClick={() => dispatch(increment())}>Tăng 1</button> <button onClick={() => dispatch(decrement())} style={{ marginLeft: "10px" }} > Giảm 1 </button> {/* 2. Tính năng bất đồng bộ sử dụng createAsyncThunk */} <button onClick={() => dispatch(incrementAsync(5))} disabled={status === "loading"} style={{ marginLeft: "10px" }} > {status === "loading" ? "Đang tải..." : "Tăng thêm 5 (Sau 1s)"} </button> <hr style={{ margin: "40px 0" }} /> {/* 3. Tính năng lấy dữ liệu bất đồng bộ sử dụng RTK Query */} <h3>Danh sách bài viết từ API</h3> {isLoading && <p>Đang tải bài viết...</p>} {isError && <p>Lỗi khi load dữ liệu!</p>} <ul style={{ listStyleType: "none", padding: 0 }}> {posts?.map((post: any) => ( <li key={post.id} style={{ margin: "5px 0" }}> {post.title} </li> ))} </ul> </div> ); } // Xuất bản Component đã được bao bọc độc lập bởi Advanced Store export default function AdvancedPage() { return ( <Provider store={advancedStore}> <AdvancedDemo /> </Provider> ); }
Kết quả
Như vậy chúng ta đã vừa hoàn thành xong việc tạo một trang ứng dụng Counter nâng cao áp dụng createAsyncThunk và RTK Query. Bạn có thể tham khảo source code tại đây.
Mở rộng
Khi tìm hiểu về xử lý bất đồng bộ trong Redux, chắc chắn bạn sẽ nghe nhắc đến Redux Saga – một thư viện rất nổi tiếng sử dụng Generator Functions (function* và yield) để quản lý side-effects. Vậy bước sang năm 2026, liệu có cần phải học thêm Redux Saga hay không?
Redux Saga còn phổ biến không?
- Thực tế: Không còn là ưu tiên cho các dự án mới.
- Redux Saga cực kỳ mạnh mẽ trong việc giải quyết các luồng async siêu phức tạp (hủy request giữa chừng, retry, điều phối các action chéo nhau…). Tuy nhiên, điểm yếu lớn nhất của Saga là độ phức tạp cao (learning curve dốc) và phải viết quá nhiều code thủ công (boilerplate).
- Năm 2026: Saga chủ yếu xuất hiện ở các dự án cũ (Legacy projects) cần bảo trì.
Xu hướng quản lý State & Gọi API năm 2026
Bức tranh công nghệ hiện tại đã tinh gọn hơn rất nhiều:
createAsyncThunk(Redux Thunk): Vẫn giữ vị trí cốt lõi cho các tác vụ bất đồng bộ linh hoạt ở phía Client nhờ sự đơn giản, tích hợp sẵn trong RTK.RTK Query: Đã trở thành chuẩn mực chung nếu dự án của bạn dùng Redux Toolkit để gọi API. RTK Query giải quyết gọn gàng 90% nhu cầu gọi API, caching, auto-refetch mà không cần viết Saga hay Thunk.- Các lựa chọn thay thế (ngoài hệ sinh thái Redux): Với các dự án React tạo mới năm 2026, các Developer cũng có xu hướng chọn cặp đôi gọn nhẹ hơn như Zustand (để quản lý client state) kết hợp TanStack Query (tên mới của React Query – để quản lý server state/API).
💡 Tóm lại: Bạn chỉ nên tập trung 100% công lực làm chủ
createAsyncThunkvàRTK Query. Bạn không cần tốn thời gian học Redux Saga nữa trừ khi công ty yêu cầu bảo trì một hệ thống cũ!
Nên chọn RTK Query hay createAsyncThunk?
Dưới đây là bảng so sánh nhanh giúp bạn quyết định ngay nên chọn RTK Query hay createAsyncThunk cho từng trường hợp cụ thể:
| Tiêu chí | RTK Query | createAsyncThunk |
| Mục đích chính | Làm việc với Data/API từ Server (CRUD) | Tác vụ Bất đồng bộ tổng quát (Client Logic) |
| Lượng code (Boilerplate) | Rất ít – Tự động sinh React Hooks (ví dụ: useGetProductsQuery()) | Nhiều hơn – Phải tự tạo Slice, handle trạng thái pending, fulfilled, rejected |
| Quản lý Cache & Loading | Tự động 100% (Tự lưu cache, tự ngắt/gọi lại API, tự cung cấp isLoading, isError) | Thủ công (Phải tự tạo state loading, error trong Redux Store) |
| Các tác vụ linh hoạt | Không tối ưu nếu tác vụ không liên quan đến API | Rất mạnh (Đọc file, làm việc với LocalStorage, Timer, SDK bên thứ 3) |
| Chuỗi xử lý (Async Workflow) | Khó ghép nối logic phức tạp giữa các request | Dễ dàng kết hợp chuỗi logic async/await nhiều bước trước khi lưu vào Store |
💡 Tóm tắt ngắn gọn để quyết định ngay:
- 90% công việc hàng ngày (Gọi API, hiển thị danh sách, thêm/sửa/xóa): Dùng RTK Query (Nhanh, gọn, tự động làm hết phần khó).
- 10% công việc còn lại (Ghi file, gọi Bluetooth/SDK, logic client phức tạp): Dùng
createAsyncThunk(Linh hoạt, can thiệp sâu).
Lời kết
Trải qua hai bài viết từ cơ bản đến nâng cao, chúng ta đã cùng nhau chinh phục trọn vẹn hệ sinh thái Redux Toolkit hiện đại kết hợp với TypeScript. Bằng cách áp dụng cấu trúc thư mục phẳng và giải pháp bọc <Provider> cô lập, bạn hoàn toàn có thể chạy song song nhiều mô hình quản lý trạng thái trong cùng một dự án mà không sợ xung đột logic.
Nhìn lại hành trình bài học nâng cao này, hãy nhớ hai quy tắc chọn “vũ khí” cốt lõi sau:
| Công cụ | Khi nào nên dùng? | Lợi ích lớn nhất |
createAsyncThunk | Khi bạn cần tự tay kiểm soát các logic xử lý chuỗi hành động bất đồng bộ phức tạp hoàn toàn ở phía Client (ví dụ: login, validate nhiều bước). | Kiểm soát sâu 3 trạng thái pending, fulfilled, rejected. |
RTK Query | Khi ứng dụng cần tương tác liên tục với hệ thống API Server để lấy dữ liệu (CRUD). | Tự động hóa 100% boilerplate code, tích hợp sẵn Caching, Auto-refetch và Loading/Error state. |
Hy vọng chuỗi bài viết này đã giúp bạn gạt bỏ nỗi sợ “code rườm rà” của Redux truyền thống và tự tin đưa Redux Toolkit vào các dự án thực tế sắp tới.
Nếu gặp bất kỳ khó khăn nào trong quá trình chạy thử Sandbox hoặc cấu hình Router, hãy để lại bình luận ngay phía dưới bài viết nhé. Chúc các bạn code vui vẻ và hẹn gặp lại ở các bài viết tiếp theo trên TienDev Blog!





