redux-saga when to use fork?
what would be the difference between the two approaches below?
export function* watchLoginUser() {
yield takeEvery(USER_LOGIN, loginUser)
}
export function* watchLogoutUser() {
yield takeEvery(USER_LOGOUT, logoutUser)
}
export function* watchGetParties() {
yield takeEvery(PARTIES_GET, getParties)
}
export default function* root() {
yield [
fork(watchLoginUser),
fork(watchLogoutUser),
fork(watchGetParties)
]
}
export default function* root() {
yield [
takeEvery(USER_LOGIN, loginUser),
takeEvery(USER_LOGOUT, logoutUser),
takeEvery(PARTIES_GET, getParties)
]
}
When do I need to use fork and when not?
In general, fork
is useful when a saga needs to start a non-blocking task. Non-blocking here means: the caller starts the task and continues executing without waiting for it to complete.
There is a variety of situations where this can be useful, but the 2 main ones are:
- grouping sagas by logical domain
- keeping a reference to a task in order to be able to cancel/join it
Your top-level saga can be an example of the first use-case. You'll likely have something like:
yield fork(authSaga);
yield fork(myDomainSpecificSaga);
// you could use here something like yield [];
// but it wouldn't make any difference here
Where authSaga
will likely include things like:
yield takeEvery(USER_REQUESTED_LOGIN, authenticateUser);
yield takeEvery(USER_REQUESTED_LOGOUT, logoutUser);
You can see that this example is equivalent to what you suggested, calling with fork
a saga yielding a takeEvery
call. But in practice, you only need to do this for code organisation purposes. takeEvery
is itself a forked task, so in most cases, this would be uselessly redundant.
An example of the second use-case would be something like:
yield take(USER_WAS_AUTHENTICATED);
const task = yield fork(monitorUserProfileUpdates);
yield take(USER_SIGNED_OUT);
yield cancel(task);
You can see in this example that the monitorUserProfileUpdates
will execute while the caller saga resumes, and gets to wait to the USER_SIGNED_OUT
action to be dispatched. It can in addition keep a reference to it in order to cancel it when needed.
For the sake of completeness, there is another way to start non-blocking calls: spawn
. fork
and spawn
differ in how errors and cancellations bubble from child to parent saga.
Usually fork
become more useful for some cases that has multiple dispatches of API calls, the reason is you can reject the those fetches by instantiating the cancel from the task, e.g. cancel(task1);
Useful if the end-user forcefully exit the application or if one of the tasks was failed that make a problem from your instructions, strategy and logic and it might be reasonable to cancel or terminate the current processing tasks on your saga;
There are 2 ways to cancel the task
base from the documentation of redux-saga Non-Blocking effect cancellation
import { take, put, call, fork, cancel } from 'redux-saga/effects'
// ...
function* loginFlow() {
while (true) {
const {user, password} = yield take('LOGIN_REQUEST')
// Non-Blocking Effect which is the fork
const task = yield fork(authorize, user, password)
const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
if (action.type === 'LOGOUT'){
//cancel the task
yield cancel(task)
yield call(Api.clearItem, 'token')
}
}
}
OR
import {call, put, fork, delay} from 'redux-saga/effects';
import someAction from 'action/someAction';
function* fetchAll() {
yield fork(fetcher, 'users');
yield fork(fetcher, 'posts');
yield fork(fetcher, 'comments');
yield delay(1500);
}
function* fetcher(endpoint) {
const res = yield call(fetchAPI, endpoint);
if (!res.status) {
throw new Error(`Error: ${res.error}`);
}
yield put(someAction({payload: res.payload}));
}
function* worker() {
try {
yield call(fetchAll);
} catch (err) {
// handle fetchAll errors
}
}
function* watcher() {
yield takeEvery(BLOGS.PUSH, worker);
}
Your welcome :)