Yêu cầu Sep 8th, 2019 1:34 a.m. 1882 1 1
  • 1882 1 1
+4

Nhờ mọi người giải thích giúp mình Redux-Saga

Chia sẻ
  • 1882 1 1

Mọi người ơi, Dạo trước, khi làm React mình có tìm hiểu về Redux-Saga và mình kiếm được ví dụ này :

https://stackblitz.com/edit/react-redux-saga-post-demo-da8hwk?file=sagas/PostsSagas.js

Flow của nó là : Ở Component phát đi một hành động đến Action , sau đó Action chuyển tiếp nó đến Saga thông qua một cái type có status là request.

Tại Saga ứng với mỗi Action đó có 2 function:

  1. watchSaga : để bắt cái action có: type = request gửi đến từ action,
  2. workSaga: để thực thi Async-Action đó => sau khi có kết quả success or fail => se PUT một trạng thái tương ứng vào trong Reducer để update lại state.

Và trong project sẽ có nhiều saga và tất cả các function watchSaga của saga sẽ được import vào trong một file gọi là rootSaga và được thực thi bằng lệnh: yield[watchSaga1, watchSaga2 , ...].

Vậy là xong ! Mình thấy rất dễ hiểu và áp dụng được ngay vào project của mình.

Tuy nhiên mấy bữa nay, có thời gian mình mới ngồi tìm hiểu thêm về thằng Saga này thì thấy trên mạng có nhiều cách viết lạ mà mình chưa biết ví dụ như: Why(true) với fork mình thực sự chưa hiểu dùng chúng trong trường hợp nào và với mục đích gì.

Mình có tìm hiểu qua trên docs của Saga nhưng vì khả năng đọc tài liệu của mình còn kém nên cung không hiểu được nhiều. Vì thế mình lên đây nhờ mọi người có thể giải thích giúp mình 2 thằng trên được không. Mình cám ơn mn nhiều !

1 CÂU TRẢ LỜI


Đã trả lời Sep 8th, 2019 4:12 a.m.
Đã được chấp nhận
+12

Cách viết while (true) phương pháp giúp bạn controll flow chạy khác so với cách viết thông thường mà bạn đang dùng. Với cách viết thông thường là định nghĩa 1 cái saga sau đó gán 1 action cho saga đó bằng cách dùng takeEvery hoặc takeLatest thì với mỗi action được dispatch nó sẽ liên tục gọi đến cái function saga mà bạn định nghĩa. Còn đối với việc sử dụng while (true) thì thay vì bạn gán trực tiếp 1 action cho 1 function saga như này:

export function* watchFetchPost() {
  yield takeEvery(types.FETCH_POST, workFetchPost);
}

Thì while (true) sẽ chạy như 1 vòng lặp vô tận và bên trong đó bạn sẽ sử dụng hàm take để lắng nghe các action được tạo ra. Một ví dụ cụ thể hơn mình lấy từ chính doc của Redux-saga như sau:

import { take, call, put } from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    return token
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  }
}

function* loginFlow() {
  while (true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const token = yield call(authorize, user, password)
    if (token) {
      yield call(Api.storeItem, {token})
      yield take('LOGOUT')
      yield call(Api.clearItem, 'token')
    }
  }
}

Khác với cách viết thông thường là mỗi khi bạn tạo ra một action là LOGIN_REQUEST thì nó sẽ lập tức chạy ngay một cái saga nào đó mà bạn gán cho actions đó như này:

export function* login() {
  yield takeEvery(types.LOGIN_SUCCESS, doSomeLogin);
}
  • Thì với cách viết dùng while như trên thì cho dù bạn có tạo ra có click cái button để tạo ra action LOGIN_REQUEST bao nhiêu lần thì nó cũng sẽ chỉ nhận đúng lần đầu tiên thôi và không chạy liên tục như cách viết thông thường.
  • Thứ 2 là vì đây là vòng lặp (lặp vô hạn) nên sau khi bạn chạy xong cái LOGIN_REQUEST thì cái saga loginFlow này nó sẽ không lắng nghe thêm bất cứ action LOGIN_REQUEST nào nữa mà chỉ đợi action LOGOUT mà thôi vì là vòng lặp mà bạn chưa chạy hết nên nó không chạy lại từ đầu được :v
  • Cuối cùng sau khi bạn gọi action LOGOUT thì nó mới kết thúc 1 vòng lặp và mới tiếp tục lắng nghe lại cái action LOGIN_REQUEST. Mọi thứ cứ tiếp diễn như thế mãi mãi.

=> Ở đây 2 action LOGIN_REQUESTLOGOUT sẽ luôn đi kèm với nhau theo đúng thứ tự trên dưới và chỉ có thể gọi lại khi mà thằng kia đã được gọi như sau:

  • LOGOUT sẽ luôn phải đợi 1 action LOGIN_REQUEST chạy trước nó và chạy xong
  • LOGIN_REQUEST sau khi chạy 1 lần cũng phải đợi cho đến khi LOGUT chạy xong thì nó mới chạy lại được

Bạn có thể thử xem ví dụ này để hiểu hơn về dfung cái while(): https://codesandbox.io/s/pulling-future-action-e7fll
Ví dụ trên chỉ có 2 action là INCREMENTDECREMENT và như mình nói ở trên cho dù bạn click bao nhiêu lần INCREMENT liên tiếp đi nữa thì nó cũng chỉ chạy duy nhất lần đầu và nó chỉ chạy lần tiếp theo khi mà bạn ấn DECREMENT. Tương tự thì DECREMENT cũng sẽ không chạy nếu bạn chưa bấm INCREMENT.


Còn về phần fork thì bạn có thể hiểu như sau cái hàm:

yiedl call()

Trong redux-saga nó là một hàm blocking nghĩ là với ví dụ dừ doc của redux-saga:

function* loginFlow() {
  while (true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const token = yield call(authorize, user, password)
    if (token) {
      yield call(Api.storeItem, {token})
      yield take('LOGOUT')
      yield call(Api.clearItem, 'token')
    }
  }
}

Trong trường hợp đang chạy cái API ở phần const token = yield call(authorize, user, password) nhưng tự nhiên người dùng lại muốn LOGOUT luôn thì như mình nói ở trên là không được vì cái call() kia là blocking nên code ở dưới sẽ không chạy cho đến khi nó chạy xong. Chính vì thế mặc dù ta bấm LOGOUT rồi nhưng nó sẽ bị bỏ qua cho đến khi chạy thành công việc gọi API. Chính vì thể ở đây ta cần dùng hàm fork() như sau:

import { fork, call, take, put } from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    yield call(Api.storeItem, {token})
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  }
}

function* loginFlow() {
  while (true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    yield fork(authorize, user, password)
    yield take(['LOGOUT', 'LOGIN_ERROR'])
    yield call(Api.clearItem, 'token')
  }
}

Khi chúng ta dùng hàm fork thì cái task gọi API kia sẽ đươc tiến hành chạy ở background và không làm chặn cái luồng chạy chính của chúng ta nữa chính ví thể code bên dưới có thể tiếp tục chạy mà không cần đợi phần gọi API chạy xong. Bạn có thể hiểu nôm na là khi dùng fork thì cái công việc mà bạn truyền vào trong đó sẽ được mang ra một chỗ khác để thực hiện chứ không thực hiện ở ngay đó nữa.

Chia sẻ
Avatar Anh Tran @thanh_tuan
Sep 8th, 2019 9:09 a.m.

Cám ơn bạn đã trợ giúp mình , mình chưa hiểu được hoàn toàn những gì bạn nói ở trên nên tối nay mình sẽ dành thời gian để đọc và hiểu chi tiết hơn về nó. Có gì không hiểu mình sẽ hỏi lại bạn vào ngày mai nhé. Mong sẽ nhận được sự trợ giúp tiếp của bạn. Chúc bạn có ngày nghỉ cuối tuần thư gian và vui vẻ 🥂 🥂 🥂

Sep 8th, 2019 9:11 a.m.

@thanh_tuan ok bạn 😄. Bạn có thể hiểu đơn giản như này:

  • while thì các action trong đó bắt buộc sẽ phải thực hiện đúng thứ tự. Như ví dụ là LOGIN_REQUEST trước rồi mới LOGOUT.
  • fork() là thay vì bạn chạy đồng bộ (sync) từ trên xuống dưới thì fork() sẽ giúp bạn chạy dạng bất đồng bộ (async) vì cái

Thêm nữa là người ta vẫn có thể viết thành 2 function là:

export function* login() {
  yield takeEvery(types.LOGIN_REQUEST, doLogin);
}

export function* logout() {
  yield takeEvery(types.LOGOUT, doLogout);
}

Tuy nhiên theo document thì cách viết trên sẽ làm logic của bạn phân tán thành nhiều chỗ đồng thời sẽ khó đọc hơn so với việc bạn dùng while vì nhìn vào hàm đó bạn có thể thấy được ngay flow chạy là LOGIN_REQUEST rồi mới LOGOUT. Trên thực tế không phải chỗ nào bạn cũng cần viết như trên mà chỉ chỗ nào nó là một flow có thứ tự như việc LOGIN, LOGOUT trong ví dụ trên

Avatar Anh Tran @thanh_tuan
Sep 8th, 2019 11:24 p.m.

@HuyDQ , mình xin hỏi ý này đầu tiên, bạn nói là:

Với cách viết thông thường là định nghĩa 1 cái saga sau đó gán 1 action cho saga đó bằng cách dùng takeEvery hoặc takeLatest thì với mỗi action được dispatch nó sẽ liên tục gọi đến cái function saga mà bạn định nghĩa

Cái này theo như mình tìm hiểu thì nó chỉ đúng với takeEvery thôi chứ còn với takeLatest nó sẽ không vị gọi liên tục nữa mà sẽ chỉ lấy lần cuối cùng thôi chứ nhỉ ?

Sep 9th, 2019 1:31 a.m.

@thanh_tuan takeLatest nó vẫn gọi API liên tục nhưng chỉ sẽ lấy kết quả cuối cùng nhé bạn còn takeEvery thì nó sẽ phụ thuộc vào kết quả nhanh chậm khi gọi API nên không đảm bảo cho bạn kết là lấy kết qả cuối cùng

Avatar Anh Tran @thanh_tuan
Sep 9th, 2019 3:50 a.m.

@HuyDQ mình test thử thì đúng như bạn nói thật, mỗi khi action được phát động -> nó vẫn gọi lên API.

OKbạn nhé, nhờ bạn giải thích mình đã hiểu hơn rất nhiều rồi.

Cám ơn bạn nhiều nha 💯

Sep 9th, 2019 4:19 a.m.

@thanh_tuan ok bạn không có gì 🤝

Avatar Anh Tran @thanh_tuan
Sep 19th, 2019 11:14 p.m.

bạn @HuyDQ ơi, bạn giúp mình thêm vấn đề này với.

Ví dụ ở trên, trong component PostsIndex, tại lifecycle componentDidMount của nó mình thực hiện gọi API để fetch data rồi push nó vào trong store , sau đó ở dưới chỗ hàm render mình lấy data trong store ra để xài.

Tuy nhiên với trường hợp của mình, mình muốn sau khi fetch data trong componentDidMount xong, thì lấy data từ store ra và đưa nó vào trong state luôn, sau đó mới lấy data từ state ra để xài.

Mình có làm ở đây: https://stackblitz.com/edit/react-redux-saga-post-demo-g5v1tu?file=components%2FPostsIndex.js

Nhưng nó không chạy như mình mong đợi, bạn có thể sửa giúp mình đoạn này được không.

Sep 20th, 2019 2:16 a.m.

@thanh_tuan nếu bạn có lý do thực sự cần thiết để lấy data từ store để dùng lại trong state thì mình nghĩ bạn nên gọi API trực tiếp từ component đó mà không cần đi qua store nữa

Avatar Anh Tran @thanh_tuan
Sep 20th, 2019 3:30 a.m.

@HuyDQ umk, mình cũng đã suy nghĩ kỹ về vấn đề này. Thực sự thì làm như vậy mình thấy rất kỳ cục (khi đã tạo store để dữ liệu vào xong lại mất công lấy dữ liệu từ store để update vào state thực sự là << stupid >>

Nhưng vấn đề của mình lại cần phải làm vậy, mình vẫn chưa biết trình bày sao để bạn hiểu được. Thôi để mình suy nghĩ thêm xem có hướng nào giải quyết ổn thỏa không.

Nhưng về cơ bản bạn có thể giải thích giúp mình tại sao mình dùng async-await rồi mà vẫn không được vậy ?

Sep 20th, 2019 4:06 a.m.

@thanh_tuan await bạn dùng ở đây không có ý nghĩa gì nhé vì bản thân hàm this.props.fetchPosts() không phải là hàm cần đến await. Hàm của bạn bản chất là chỉ dispatch 1 cái action thôi còn lại làredux-saga sẽ lắng nghe action đó và phản hồi với hành động tương ứng chứ nó không giống như bạn viết 1 function bình thường và chạy lần lượt từ trên xuống dưới như trong code của bạn:

await this.props.fetchPosts(); // Saga xử lý cái này
const {posts} = this.props; // Cái này chạy luôn chứ không đợi saga xử lý xong -> posts = []
this.setState({posts})
Avatar Anh Tran @thanh_tuan
Sep 20th, 2019 12:53 p.m.

@HuyDQ tks bạn nhiều nha

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí