Call effect redux saga thường được sử dụng khi nào năm 2024

Mình sẽ không nói về Redux Saga nữa vì hầu hết mọi người nếu đã từng sử dụng qua Redux thì đều biết đến middleware này. Mình đã từng sử dụng qua cả redux-saga và redux-thunk thì thấy thunk rõ ràng đơn giản và dễ hiểu hơn. Mình ít sử dụng và cũng không nắm rõ cách saga hoạt động, vì vậy bài viết này sẽ cố gắng mổ xẻ nhưng thành phần cơ bản tạo nên redux-saga. Lưu ý rằng phần code bên dưới là implementation của mình trước khi đọc source code của

Task trong saga là gì?

Nhân vật chính trong redux-saga là các task. Một task hợp lệ phải là  generator function. Saga sẽ thực thi các task mà bạn cung cấp khi có action phù hợp được dispatch đến store.

Ví dụ khi có một action được dispatch đến store, saga trước khi chuyển action đến store sẽ can thiệp bằng hàm triggerWatchers. Một watcher chỉ một nhiệm vụ đơn giản là observe (quan sát) các action được dispatch đến store và thực thi callback khi có action match với chỉ dẫn của mình.

Ví dụ takeEvery sẽ tạo ra 1 watcher với callback là testCall. Khi có một action được dispatch trùng khớp với type INCREMENT, lúc đó callback gắn với watcher sẽ được thực thi.

Thế saga thực thi các task như thế nào?

Task trong saga thực chất là một generator function, vì vậy phải có một hàm khác (saga driver) chịu trách nhiệm thực thi các generator đó.

Phiên bản đơn giản của driver có thể được viết như sau. Một vòng while loop liên tục gọi vào lấy giá trị next của iterator cho đến khi nào iterator kết thúc. Một task có thể communicate với driver của nó bằng cách yield những giá trị đặc biệt, còn được gọi là saga effect. Ví dụ một action đơn giản như sau:

Khi nhận được những giá trị này driver sẽ xử lý phù hợp với từng loại action cụ thể. Ví dụ cả với fork và call, driver sẽ đều thực thi callback mà hai effect này cung cấp. Tuy nhiên ở driver sẽ đợi (await) kết quả trả về của call còn fork thì không. Từ đó ta có tính chất blocking của call và non-blocking của fork.

Trong driver còn lưu lại cả state của task đang thực thi ví dụ như trạng thái hiện tại là gì, đang có những attached task nào được thực thi,… Trong quá trình thực thi thì driver có thể huỷ task bất cứ lúc nào (set status thành CANCELED) hoặc huỷ các attached task (gọi callback cancel được các task cung cấp). Điều này khiên saga trở nên rất mạnh mẽ trong việc flow control. Ngoài ra task còn có 1 phần logic liên quan đến xử lý lỗi mà mình chưa đọc hết cũng như chưa nghĩ ra cách để implement 1 cách phù hợp. Đại để thì error handling sẽ kiểm soát hành vi của các task trong trường hợp có uncaught error xuất hiện. Task có cancel không? Nếu có cancel thì các task con có được cancel không? Task có thông báo lỗi này với parent task không? Đó là 1 vài câu hỏi mà phần error handling này sẽ phải trả lời.

Một vài effect tiêu biểu

Nhờ vào hệ thống effect đa dạng mà saga có thể biểu diễn những logic vô cùng phức tạp. Những effect đơn giản như call, fork, cancel hay put khá dễ để implement. Những effect cơ bản này có thể được kết hợp với nhau để tạo nên những effect phức tạp hơn như takeEvery hay takeLatest. Ví dụ takeEvery có thể được viết từ take và fork như sau:

Logic của takeLatest cũng tương tự nhưng sẽ có thêm phần cancel task đang được thực thi dở nếu có 1 task mới được sinh ra.

Take là một effect mình gặp khá nhiều khó khăn để implement. Cụ thể thì take sẽ cần làm hai việc, một là tạm dừng task hiện tại, hai là thêm một watcher để lắng nghe pattern chỉ định.

Bên trên là vài điểm mình thấy khá hay ho về saga. Saga còn nhiều module rất đáng đọc ví dụ như scheduler hay channel. Saga rõ ràng khó hơn đối với người bắt đầu nhưng về lâu dài tình tường mình của saga sẽ khiến việc debug trở nên rất dễ dàng.

Redux saga về bản chất nó là một middleware library để handle các side effect trong trong ReactJS (Side effect các bạn có thể hiểu đơn giản là các xử lý bất đồng bộ như là call API, sử dụng setTimeout, setInterval,…).

  • Redux saga được xây dựng dựa trên các javascript generator function, vì thế các bạn có thể tìm hiểu thêm ở link này để hiểu thêm về loại function này nhé, nó khác gì so với các function bình thường.
  • Đối với generator function, nó có một vài helper mà mình cần phải biết trước khi bắt tay vào thực hành.
  • takeEvery() : thực thi và trả lại kết quả của mọi actions được gọi.
  • takeLastest() : có nghĩa là nếu chúng ta thực hiện một loạt các actions, nó sẽ chỉ thực thi và trả lại kết quả của của actions cuối cùng.
  • take() : tạm dừng cho đến khi nhận được action.
  • put() : dispatch một action.
  • call(): gọi function. Nếu nó return về một promise, tạm dừng saga cho đến khi promise được giải quyết.
  • race() : chạy nhiều effect đồng thời, tuy nhiên chỉ lấy kết quả của effect nhanh nhất, và hủy kết quả của những effect còn lại. Các bạn có thể hình dung như một cuộc đua, và nó chỉ qua tâm đến người thắng cuộc.

Ok mình sẽ bắt tay vào thực hành!

1. Set up redux và tích hợp redux-saga

Bây giờ chúng ta sẽ tạo ra một ứng dụng nho nhỏ show ra list các bài Post lấy data từ APi sử dụng Redux-saga kết hợp với axios nhé.

Mình sẽ lần lượt tạo các file như bên dưới. Đầu tiên mình sẽ tạo ra 3 file đại diện cho 3 thành phần cơ bản của redux nhé, bao gồm: action.js, constant.js, reducer.js.

Call effect redux saga thường được sử dụng khi nào năm 2024

  • File action.js

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

import {GET_LIST_POST, GET_LIST_POST_SUCCESS} from './constant'; export const getListPost = (payload) => { return { type: GET_LIST_POST, payload, } } export const getListPostSuccess = (payload) => { return { type: GET_LIST_POST_SUCCESS, payload, } }

  • File constant.js

1 2

export const GET_LIST_POST = 'GET_LIST_POST'; export const GET_LIST_POST_SUCCESS = 'GET_LIST_POST_SUCCESS';

  • File reducer.js

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

const postsReducer = (state = INITIAL_STATE, action) => { switch(action.type) { case GET_LIST_POST: return { ...state, load: true, }; case GET_LIST_POST_SUCCESS: const {data} = action.payload; return { ...state, posts: data, load: false, }; default: return state; } } export default postsReducer;

Tiếp theo, mình sẽ tạo phần config cho redux, các bạn tạo một thư mục là redux, trong đó chứa 2 file là store.js và rootReducer.js

  • File store.js

1 2 3 4 5 6 7 8 9 10

import { createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; import rootReducer from './rootReducer'; import postsSaga from '../saga'; const sagaMiddleware = createSagaMiddleware(); const store = createStore(rootReducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(postsSaga); export default store;

  • File rootReducer.js

1 2 3 4 5 6 7

import { combineReducers } from 'redux'; import postsReducer from '../reducer'; const rootReducer = combineReducers({ posts: postsReducer, }); export default rootReducer;

Ở file store js mình đã tiến hành tích hợp redux saga vào, các bạn ra teminal chạy lệnh npm install redux-saga hoặc lệnh yarn add redus-saga để cài đặt redux-saga nhé. Tiếp theo tại file index.js mình import thằng Provider của Redux vào và wrap nó ở component App như bình thường nhé!

import {GET_LIST_POST, GET_LIST_POST_SUCCESS} from './constant'; export const getListPost = (payload) => { return { type: GET_LIST_POST, payload, } } export const getListPostSuccess = (payload) => { return { type: GET_LIST_POST_SUCCESS, payload, } }

0

import {GET_LIST_POST, GET_LIST_POST_SUCCESS} from './constant'; export const getListPost = (payload) => { return { type: GET_LIST_POST, payload, } } export const getListPostSuccess = (payload) => { return { type: GET_LIST_POST_SUCCESS, payload, } }

1

  • Tiếp theo, tạo file saga.js

import {GET_LIST_POST, GET_LIST_POST_SUCCESS} from './constant'; export const getListPost = (payload) => { return { type: GET_LIST_POST, payload, } } export const getListPostSuccess = (payload) => { return { type: GET_LIST_POST_SUCCESS, payload, } }

2

import {GET_LIST_POST, GET_LIST_POST_SUCCESS} from './constant'; export const getListPost = (payload) => { return { type: GET_LIST_POST, payload, } } export const getListPostSuccess = (payload) => { return { type: GET_LIST_POST_SUCCESS, payload, } }

3

Nhiệm vụ của file này là lắng nghe nếu có action getListPost được dispatch, thì nó bắt lấy để handle việc call API, khi call API thì thường sẽ có 2 trường hợp xảy ra trường hợp như khối try catch ở trên, nếu call success thì nó vô khối try, lúc này mình dispatch một action mới là getListPostSuccess để cập nhật data get được từ APi vô redux store, trong trường hợp call error thì nó sẽ vô khối catch (vấn đề mình xin trình bày trong một bài khác).

2. Tích hợp Axios để call APi

Để handle việc call API, chúng ta sẽ tạo ra một file là postAPI.js

import {GET_LIST_POST, GET_LIST_POST_SUCCESS} from './constant'; export const getListPost = (payload) => { return { type: GET_LIST_POST, payload, } } export const getListPostSuccess = (payload) => { return { type: GET_LIST_POST_SUCCESS, payload, } }

4

import {GET_LIST_POST, GET_LIST_POST_SUCCESS} from './constant'; export const getListPost = (payload) => { return { type: GET_LIST_POST, payload, } } export const getListPostSuccess = (payload) => { return { type: GET_LIST_POST_SUCCESS, payload, } }

5

Trong file này mình sử dụng một HTTP client dựa trên promise để call API đó là Axios, Chạy lệnh npm install –save axios hoặc yarn add axios để cài đặt thư viện.

3. Render kết quả

import {GET_LIST_POST, GET_LIST_POST_SUCCESS} from './constant'; export const getListPost = (payload) => { return { type: GET_LIST_POST, payload, } } export const getListPostSuccess = (payload) => { return { type: GET_LIST_POST_SUCCESS, payload, } }

6

import {GET_LIST_POST, GET_LIST_POST_SUCCESS} from './constant'; export const getListPost = (payload) => { return { type: GET_LIST_POST, payload, } } export const getListPostSuccess = (payload) => { return { type: GET_LIST_POST_SUCCESS, payload, } }

7

Ở phần render ra kết quả này mọi người có thể thấy nếu đang call API tức là biến load đang là true thì nó sẽ show ra một đoạn text “Data is loading from API…”, trong trường hợp call API xong rồi thì biến load sẽ được set thành false và show ra kết quả như bên dưới.