Nhờ mọi người giải thích giúp mình Redux-Saga
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:
watchSaga: để bắt cáiaction có: type = requestgửi đến từaction,workSaga: để thực thiAsync-Actionđó => sau khi có kết quảsuccessorfail=> sePUTmột trạng thái tương ứng vào trongReducerđể update lạistate.
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
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
whilenhư trên thì cho dù bạn có tạo ra có click cái button để tạo ra actionLOGIN_REQUESTbao 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_REQUESTthì cái sagaloginFlownày nó sẽ không lắng nghe thêm bất cứ actionLOGIN_REQUESTnào nữa mà chỉ đợi actionLOGOUTmà 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
LOGOUTthì 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 actionLOGIN_REQUEST. Mọi thứ cứ tiếp diễn như thế mãi mãi.
=> Ở đây 2 action LOGIN_REQUEST và LOGOUT 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:
LOGOUTsẽ luôn phải đợi 1 actionLOGIN_REQUESTchạy trước nó và chạy xongLOGIN_REQUESTsau khi chạy 1 lần cũng phải đợi cho đến khiLOGUTchạ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à INCREMENT và DECREMENT 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.
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ẻ

@thanh_tuan ok bạn
. Bạn có thể hiểu đơn giản như này:
whilethì các action trong đó bắt buộc sẽ phải thực hiện đúng thứ tự. Như ví dụ làLOGIN_REQUESTtrước rồi mớiLOGOUT.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
@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
sagasau đó gán 1actionchosagađó bằng cách dùngtakeEveryhoặctakeLatestthì với mỗiactionđượcdispatchnó sẽ liên tục gọi đến cái functionsagamà 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ỉ ?
@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
@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 
@thanh_tuan ok bạn không có gì 
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.
@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
@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 ?
@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})