Chắc hẳn nếu bạn đã làm việc với React một thời gian, bạn sẽ biết quản lý state là một phần không thể thiếu và đôi khi khá "nhức não". Trong bài viết này, mình muốn chia sẻ với các bạn về hai công cụ cực kỳ hữu ích: Redux Toolkit (RTK) và Redux Saga. Cả hai đều hỗ trợ mạnh mẽ trong việc quản lý trạng thái (state management) và xử lý các side effect một cách dễ dàng hơn trong các dự án React.
Nếu bạn đã quen thuộc với Redux cơ bản hoặc đang bắt đầu tìm hiểu về cách quản lý state trong React, thì Redux Toolkit và Redux Saga sẽ mở ra cho bạn những cách tiếp cận mới, giảm bớt phức tạp và giúp code của bạn trở nên rõ ràng hơn. Hãy cùng mình khám phá cách cài đặt và sử dụng chúng trong bài viết này nhé.
1. Giới thiệu về Redux Toolkit và Redux Saga
1.1 Redux Toolkit là gì?
Redux Toolkit (RTK) là bộ công cụ chính thức của Redux, nhằm đơn giản hóa và tối ưu hóa quy trình phát triển ứng dụng với Redux. Thay vì phải viết hàng loạt action creators, reducers, và middleware một cách thủ công, RTK cung cấp các phương thức tiện lợi như createSlice
, createAsyncThunk
, và configureStore
, giúp chúng ta tự động hóa hầu hết những công việc này.
Chi tiết về các tính năng của Redux Toolkit:
- configureStore: Tạo store Redux với mặc định đã được cấu hình sẵn (như devtools, middleware,...).
- createSlice: Tạo ra reducer và action cho một phần của state.
- createAsyncThunk: xử lý bất đồng bộ (ví dụ: lấy dữ liệu từ API) dễ dàng.
- Cung cấp sẵn những best practice trong cấu hình Redux.
1.2 Redux Saga là gì?
Redux Saga là một middleware của Redux, giúp quản lý các side effect phức tạp (như call API, tương tác với side effect) trong ứng dụng. Redux Saga sử dụng khái niệm "generator function" để quản lý logic bất đồng bộ dễ hiểu và có tổ chức.
- Quản lý những luồng logic bất đồng bộ phức tạp (gọi API, điều phối nhiều action, concurrency, retry…).
- Tách riêng logic side effect khỏi component và reducer, làm code dễ hiểu và dễ test hơn.
2. So sánh Redux Toolkit và Redux Saga
Mặc dù Redux Toolkit và Redux Saga đều nhằm phục vụ ứng dụng Redux, chúng có những vai trò khác nhau:
2.1 Redux Toolkit
Redux Toolkit giúp bạn đơn giản hóa việc thiết lập và sử dụng Redux, giảm boilerplate và tự động cung cấp các best practice.
Lợi ích:
- Tạo và quản lý store, state reducer dễ dàng.
- createAsyncThunk giúp xử lý hành động async đơn giản (như gọi API).
Khi nào bạn nên dùng:
- Khi bạn muốn thiết lập Redux nhanh chóng và chuẩn.
- Khi ứng dụng chủ yếu yêu cầu logic bất đồng bộ đơn giản.
- Khi bạn là người mới bắt đầu hoặc muốn code Redux gọn gàng, dễ bảo trì.
Ví dụ như nếu bạn đang build ứng dụng chỉ có logic bất đồng bộ đơn giản, cần fetch data và update state, createAsyncThunk là đủ.
2.2 Redux Saga
Redux Saga làm middleware cho Redux để quản lý side effect phức tạp và logic bất đồng bộ.
Lợi ích: sử dụng generator function để xử lý luồng side effect phức tạp:
- nhiều API call đồng thời
- retry, cancellation
- quản lý logic phức tạp (like concurrency).
Khi nào bạn nên dùng:
- Khi ứng dụng có logic bất đồng bộ phức tạp vượt quá khả năng của createAsyncThunk.
- Khi cần kiểm soát luồng side effect chi tiết, sử dụng generator function để viết code rõ ràng hơn.
- Khi cần các tính năng như debounce, throttle, retry, cancellation logic.
Ví dụ nếu ứng dụng có logic side effect phức tạp, cần concurrency, cancellation, retry thì bạn cần đến redux saga.
Bạn cũng có thể sử dụng kết hợp cả Redux Saga và Redux Toolkit với nhau:
- RTK giúp cấu hình store và tạo slice dễ dàng.
- Redux Saga đảm nhiệm xử lý logic bất đồng bộ phức tạp mà createAsyncThunk không đáp ứng được.
3. Hướng dẫn sử dụng Redux Toolkit trong dự án React
Đầu tiên, bạn cần có một source base trước thì ở bài trước mình đã hướng dẫn bạn setup source React Vite rồi. Nên nếu bạn chưa biết thì đừng lo, hãy xem tại đây nhé.
Khi bạn có source rồi thì, cùng mình cài đặt thư viện redux toolkit nha:
yarn add @reduxjs/toolkit react-redux
B1: Cấu hình store với Redux Toolkit
import { configureStore } from '@reduxjs/toolkit';
// Chúng ta sẽ thêm reducer sau
const store = configureStore({
reducer: {}, // placeholder
});
export default store;
// Các kiểu dữ liệu để sử dụng trong hooks
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
- configureStore: tạo store với cấu hình sẵn, có thể thêm reducer.
- RootState: lấy kiểu state root từ store.
- AppDispatch: lấy kiểu cho dispatch.
B2: Cập nhật src/index.tsx
để kết nối store với React:
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store'; // import store
ReactDOM.render(
<React.StrictMode>
<Provider store={store}> {/* Kết nối store tới React */}
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
B3: Tạo Slice và Reducer sử dụng Redux Toolkit
createSlice cho phép chúng ta tạo actions và reducers chỉ trong một nơi.
// src/features/user/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface UserState {
name: string;
age: number;
}
const initialState: UserState = {
name: '',
age: 0,
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
updateUser: (state, action: PayloadAction<{ name: string; age: number }>) => {
// Cập nhật state user
state.name = action.payload.name;
state.age = action.payload.age;
},
},
});
export const { updateUser } = userSlice.actions;
export default userSlice.reducer;
interface UserState: định nghĩa kiểu dữ liệu state user.
initialState: trạng thái ban đầu.
createSlice:
- name: định danh slice (cần duy nhất trong root state).
- reducers: nơi định nghĩa reducer. Ở đây có updateUser cập nhật state user.
- userSlice.actions: tự động tạo action creators dựa trên reducer.
- userSlice.reducer: reducer của slice, sẽ được thêm vào store.
B4: Thêm reducer vào store
// src/store.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './features/user/userSlice';
const store = configureStore({
reducer: {
user: userReducer,
},
});
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Thêm user: userReducer vào reducer.
B5: Tạo Async Thunk để xử lý logic bất đồng bộ. Ví dụ get user từ API
// src/features/user/userSlice.ts
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
interface UserState {
name: string;
age: number;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
}
const initialState: UserState = {
name: '',
age: 0,
status: 'idle',
};
// Tạo async thunk để fetch user
export const fetchUser = createAsyncThunk(
'user/fetchUser',
async (userId: number) => {
const response = await fetch(`https://api.example.com/users/${userId}`);
return (await response.json()) as { name: string; age: number };
}
);
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
updateUser: (state, action: PayloadAction<{ name: string; age: number }>) => {
state.name = action.payload.name;
state.age = action.payload.age;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.status = 'succeeded';
state.name = action.payload.name;
state.age = action.payload.age;
})
.addCase(fetchUser.rejected, (state) => {
state.status = 'failed';
});
},
});
export const { updateUser } = userSlice.actions;
export default userSlice.reducer;
- createAsyncThunk tạo action types (pending, fulfilled, rejected) tự động.
- extraReducers: Sử dụng để xác định reducer tương ứng với action từ createAsyncThunk.
- fetchUser: Logic bất đồng bộ fetch dữ liệu user từ API.
B6: Sử dụng state và dispatch action trong component React
// src/App.tsx
import React, { useState } from 'react';
import { useAppDispatch, useAppSelector } from './hooks';
import { updateUser, fetchUser } from './features/user/userSlice';
function App() {
const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.user);
const [userId, setUserId] = useState(1);
return (
<div>
<h1>User Info</h1>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<p>Status: {user.status}</p>
<button onClick={() => dispatch(updateUser({ name: 'Alice', age: 25 }))}>
Update User
</button>
<div>
<input
type="number"
value={userId}
onChange={(e) => setUserId(Number(e.target.value))}
/>
<button onClick={() => dispatch(fetchUser(userId))}>
Fetch User
</button>
</div>
</div>
);
}
export default App;
- useAppSelector((state) => state.user): trích xuất state user.
- updateUser và fetchUser action được dispatch để cập nhật state.
// src/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Tạo typed hook dùng cho dispatch và selector
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
- useAppDispatch: Dispatch actions với kiểu chính xác (AppDispatch).
- useAppSelector: Chọn state từ store với kiểu chính xác (RootState).
4. Hướng dẫn sử dụng Redux Saga trong dự án React
Khi bạn đã có source base React Vite trong tay rồi thì tải package này nha
yarn add redux react-redux redux-saga
B1: Tạo Store và cấu hình Redux Saga
// src/store.ts
import { createStore, applyMiddleware, combineReducers } from 'redux';
import createSagaMiddleware from 'redux-saga';
import userReducer from './features/user/userReducer';
import rootSaga from './rootSaga';
const sagaMiddleware = createSagaMiddleware();
const rootReducer = combineReducers({
user: userReducer,
});
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
export default store;
- combineReducers: kết hợp nhiều reducer.
- createSagaMiddleware: tạo middleware cho saga.
- applyMiddleware: áp dụng saga middleware cho store.
B2: Tạo Saga để xử lý side effect
// src/features/user/userSaga.ts
import { call, put, takeLatest } from 'redux-saga/effects';
import { fetchUserSuccess, fetchUserFailure } from './userActions';
// giả lập gọi API
function fetchUserFromApi(userId: number) {
return fetch(`https://api.example.com/users/${userId}`).then(response =>
response.json()
);
}
function* fetchUser(action: { type: string; payload: number }) {
try {
const userData = yield call(fetchUserFromApi, action.payload);
yield put(fetchUserSuccess(userData));
} catch (e) {
yield put(fetchUserFailure('Failed to fetch user'));
}
}
function* userSaga() {
yield takeLatest('FETCH_USER_REQUEST', fetchUser);
}
export default userSaga;
- fetchUser: Saga generator function, gọi API và dispatch action success/failure.
- takeLatest: Lắng nghe action 'FETCH_USER_REQUEST'. Nếu action này được dispatch nhiều lần liên tiếp, nó sẽ chỉ giữ lại lần cuối.
B3: Kết hợp Redux Saga với reducer và action: Bạn cần định nghĩa actions và reducers tương ứng, và integrate saga.
// src/features/user/userActions.ts
export const fetchUserRequest = (userId: number) => ({
type: 'FETCH_USER_REQUEST',
payload: userId,
});
export const fetchUserSuccess = (userData: { name: string; age: number }) => ({
type: 'FETCH_USER_SUCCESS',
payload: userData,
});
export const fetchUserFailure = (error: string) => ({
type: 'FETCH_USER_FAILURE',
payload: error,
});
// src/features/user/userReducer.ts
interface UserState {
name: string;
age: number;
loading: boolean;
error: string | null;
}
const initialState: UserState = {
name: '',
age: 0,
loading: false,
error: null,
};
function userReducer(state = initialState, action: any): UserState {
switch (action.type) {
case 'FETCH_USER_REQUEST':
return { ...state, loading: true, error: null };
case 'FETCH_USER_SUCCESS':
return { ...state, loading: false, ...action.payload };
case 'FETCH_USER_FAILURE':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
export default userReducer;
B4: Sử dụng state và dispatch action trong component React: sử dụng useSelector để lấy state và useDispatch để dispatch action.
// src/App.tsx
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUserRequest } from './features/user/userActions';
import { RootState } from './store';
function App() {
const dispatch = useDispatch();
const user = useSelector((state: RootState) => state.user);
const [userId, setUserId] = useState(1);
const handleFetchUser = () => {
dispatch(fetchUserRequest(userId));
};
return (
<div>
<h1>User Info</h1>
{user.loading ? <p>Loading...</p> : user.error ? <p>{user.error}</p> : (
<>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
</>
)}
<input
type="number"
value={userId}
onChange={(e) => setUserId(Number(e.target.value))}
/>
<button onClick={handleFetchUser}>Fetch User</button>
</div>
);
}
export default App;
- Sử dụng useSelector để chọn state user.
- useDispatch để dispatch fetchUserRequest.
- user.loading, user.error được sử dụng để hiển thị trạng thái.
5. Kết luận
Vậy là chúng ta vừa đi qua chặng đường khám phá cách sử dụng Redux Toolkit và Redux Saga trong dự án React. Mình hi vọng rằng bài viết này đã giúp bạn hiểu rõ hơn và tự tin hơn khi tích hợp Redux Toolkit và Redux Saga vào dự án React.
Việc lựa chọn công cụ phụ thuộc vào nhu cầu và quy mô ứng dụng bạn build, nhưng dù bạn chọn gì, việc có một kiến thức vững về cả hai sẽ giúp bạn xây dựng ứng dụng React mạnh mẽ và linh hoạt hơn.
Các bài viết liên quan:
Bài viết liên quan
Vercel là gì? Hướng dẫn deploy dự án Next.js bằng Vercel
Dec 07, 2024 • 14 min read
So sánh giữa HOCs, Render Props và Hooks.
Dec 05, 2024 • 8 min read
Render Props pattern là gì? Hướng dẫn sử dụng Render Props
Dec 03, 2024 • 8 min read
HOCs Pattern là gì? Hướng dẫn triển khai Hocs Pattern trong dự án React
Dec 02, 2024 • 7 min read
Hooks Pattern là gì? Hướng dẫn áp dụng Hooks Pattern trong dự án React
Nov 28, 2024 • 11 min read
Promise là gì? Hướng dẫn sử dụng Promise trong dự án React
Nov 27, 2024 • 7 min read