Facebook Pixel

Hướng dẫn sử dụng Redux Toolkit và Redux Saga trong dự án React

15 Nov, 2024

Tran Thuy Vy

Frontend Developer

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

Hướng dẫn sử dụng Redux Toolkit và Redux Saga trong dự án React

Mục Lục

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:

Bash
yarn add @reduxjs/toolkit react-redux

B1: Cấu hình store với Redux Toolkit

Typescript
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:

TSX
// 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.

Typescript
// 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

Typescript
// 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

Typescript
// 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

TSX
// 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.
Typescript
// 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

Bash
yarn add redux react-redux redux-saga

B1: Tạo Store và cấu hình Redux Saga

Typescript
// 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

Typescript
// 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.

Typescript
// 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,
});
Typescript
// 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.

TSX
// 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

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab