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 = request
gửi đến từaction
,workSaga
: để thực thiAsync-Action
đó => sau khi có kết quảsuccess
orfail
=> sePUT
mộ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
while
như trên thì cho dù bạn có tạo ra có click cái button để tạo ra actionLOGIN_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 sagaloginFlow
này nó sẽ không lắng nghe thêm bất cứ actionLOGIN_REQUEST
nào nữa mà chỉ đợi actionLOGOUT
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 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:
LOGOUT
sẽ luôn phải đợi 1 actionLOGIN_REQUEST
chạy trước nó và chạy xongLOGIN_REQUEST
sau khi chạy 1 lần cũng phải đợi cho đến khiLOGUT
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à 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:
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ớ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
saga
sau đó gán 1action
chosaga
đó bằng cách dùngtakeEvery
hoặctakeLatest
thì với mỗiaction
đượcdispatch
nó sẽ liên tục gọi đến cái functionsaga
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ỉ ?
@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})