How to grab the invoking saga from within a saga - redux

Lets say we have
function* saga1() {
yield call(saga2)
}
function* saga2() {
debugger;
}
Whats the best way to visualize how sagas are setup/structured.

Related

Why ESC generator in Redux Saga waits for promise to be fulfilled?

I am reading Redux Saga instruction docs as the below link:
https://redux-saga.js.org/docs/introduction/BeginnerTutorial/
In the code in sagas.js, incrementAsync() waits for delay() which is a promise before going to the next yield. ES6 generator is not an async function. So why this happens?
import { put, takeEvery, all } from 'redux-saga/effects'
const delay = (ms) => new Promise(res => setTimeout(res, ms))
function* helloSaga() {
console.log('Hello Sagas!')
}
function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
// notice how we now only export the rootSaga
// single entry point to start all Sagas at once
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
}
Delay is not a promise. It's a saga effect. It signals to the saga middleware that you would like your saga to wait until at least that many MS have passed before re-entering your saga. It's not an async call. If you didn't yield it you could do:
console.log("First");
const foo = delay(5000);
console.log("Second");
and you'd see that the "Second" is logged immediately after the first (not 5 seconds later).

How to get the result of a Saga?

Let's say I have a modal with a button that dispatches an action if you click on it. And I'd like to know the result of the action dispatched: e.g. if it was successful I'll close the modal and to something different otherwise.
With redux-thunk my action would look something like:
export const deleteObjects = () => {
return (dispatch, getState) => {
try {
...
dispatch(setObjects([]));
return true
} catch (e) {
return false
}
}
};
so I could use the result in my component. But how to do the same with redux-sagas? As far as I know, you can use sagas with watchers.
One solution I could think of is to pass a callback function to the action creator and call it inside of saga. Like this:
export const deleteObjects = (callback) => ({
type: DELETE_OBJECTS,
callback
});
export function* deleteObjectsAsync({callback}) {
try {
...
put(setObjects([]))
yield call(callback, true)
} catch (err) {
yield call(callback, false)
}
}
function* watchGetPlaces() {
yield takeEvery(DELETE_OBJECTS, deleteObjectsAsync)
}
Is this a valid solution or there is a more adequate way to do so?
I do not recommend your proposed solution. Passing callback functions is one of the precise things redux-saga tries to prevent the developer to have to deal with.
One clean solution is to wrap your modal closing functionality into its own saga that can be invoked by an action. I'm not sure how you open your modals, but on our apps we dispatch actions to open and close modals. Doing this enables connected components and sagas can manipulate any modal. Sagas are designed to handle side effects. Closing a modal is a side effect. Therefore, a saga is a perfect place to put closing modal logic.
Check out this boilerplate:
export const closeModal = () => ({
type: CLOSE_MODAL,
});
function* onCloseModal() {
// Your logic for closing modals goes here
}
function* closeModalSaga() {
yield takeEvery(CLOSE_MODAL, onCloseModal)
}
export const deleteObjects = () => ({
type: DELETE_OBJECTS,
});
export function* deleteObjectsAsync() {
try {
...
yield put(setObjects([]))
yield put(closeModal());
} catch (err) {
// Your "do-something-else" logic here
// I'd recommend dispatching another action
}
}
function* watchGetPlaces() {
yield takeEvery(DELETE_OBJECTS, deleteObjectsAsync)
}

Reuse saga logic

We have a general Form component, whith an accompanying saga to handle validation and submission
function* validateAndSubmit(action) {
const errors = clientSideValidate(action.values);
if (errors) {
return yield put({type: SUBMIT_FAILED, formKey: action.formKey, errors: errors});
}
try {
const response = yield call(submitToTargetUrl(action.values, action.url));
if (response.errors) {
return yield put({type: SUBMIT_FAILED, formKey: action.formKey, errors: response.errors});
}
yield put({type: SUBMIT_SUCCESS, formKey: action.formKey});
} catch (e) {
yield put({type: SUBMIT_FAILED, formKey: action.formKey, errors: [e.message]});
}
}
function* form() {
yield takeEvery(SUBMITTED, validateAndSubmit);
}
Now, we have another component, say UserForm that wraps the general Form component. On submission, we want to Submit the form to the backend and fetch some data from an external API concurrently, wait for both to complete, and then dispatch some action. This logic will live in another Saga in some other file. What would be the right pattern for reusing the validateAndSubmit logic? Is there some way to do this:
function* handleUserFormSubmit(action) {
const [submitResult, fetchResult] = yield all([
call(validateAndSubmitSaga),
call(fetchOtherData),
]);
// ...test for successful results for both
if (allIsGood) {
yield put({type: ALL_IS_GOOD});
}
}
function* userForm() {
yield takeEvery(USER_FORM_SUBMITTED, handleUserFormSubmit);
}
Thanks!
I would suggest to create a reusable validateAndSubmit function that would handle the validation and submission, then would return an error if any. And then, have a form submit saga effect that uses this function.
async function reusableValidateAndSubmit(formValues, submitUrl) {
try {
const errors = clientSideValidate(formValues);
if (errors) {
return errors;
}
const response = await submitToTargetUrl(formValues, submitUrl);
if (response.errors) {
return response.errors;
}
return null;
} catch (e) {
console.error('#reusableValidateAndSubmit: ', e);
return [e.message];
}
}
function* handleFormSubmitSaga(action) {
try {
const { values, url, formKey } = action;
const errors = yield call(reusableValidateAndSubmit, values, url);
if (errors) {
return yield put({type: SUBMIT_FAILED, formKey: formKey, errors: errors});
}
return yield put({type: SUBMIT_SUCCESS, formKey: formKey});
} catch (e) {
return yield put({type: SUBMIT_FAILED, formKey: action.formKey, errors: [e.message]});
}
}
function* form() {
yield takeEvery(SUBMITTED, handleFormSubmitSaga);
}
For the handleUserFormSubmit, I'm not just quite sure if in your use case, you want for the validateAndSubmitSaga to fail if fetchOtherData fails, and vice versa. Using redux-saga's all() would bring this effect as it behaves like Promise.all().
A snippet on the return value of Promise.all() from MDN:
This returned promise is then resolved/rejected asynchronously (as soon as the stack is empty) when all the promises in the given iterable have resolved, or if any of the promises reject.
Supposedly, it is your expected behavior, and having implemented the code above. You could just reuse reusableValidateAndSubmit function
function* handleUserFormSubmit(action) {
const [submitError, fetchResult] = yield all([
call(reusableValidateAndSubmit, action.values, action.url),
call(fetchOtherData),
]);
// ...test for successful results for both
// submitError is null if submit was a success
// fetchResult must have a value or return true if was a success
if (!submitError && fetchResult) {
yield put({type: ALL_IS_GOOD});
}
}
May I also suggest having a look at some form frameworks that you could partner with redux (i.e. redux-form) as they could also help in some use cases.
We ended up with a slightly different solution. Instead of call-ing we're take-ing:
function* handleUserFormSubmit(action) {
const [submitResult, fetchResult] = yield all([
yield take(SUBMIT_SUCCESS),
yield take(FETCH_OTHER_DATA_SUCCESS),
]);
// ...test for successful results for both
if (allIsGood) {
yield put({type: ALL_IS_GOOD});
}
}
function* userForm() {
yield takeEvery(USER_FORM_SUBMITTED, handleUserFormSubmit);
}
This way the other Saga can do its thing undisturbed, an this saga can React according to its own logic

Redux Saga - dispatching action from call function

Is there a way in redux saga to dispatch an action from a function that is being called by call effect to the global store?
for example:
export function* login({ payload }) {
try {
// from API.post I want to be able to dispatch an action that I will handle in a different saga.
const resp = yield call(API.post, 'api/1/login', {
email: payload.email,
password: payload.password,
});
yield put(AuthActions.loginSuccess(resp.token));
} catch (error) {
yield put(AuthActions.loginError(error));
}
}
I know that I can use saga's channel, but if I pass channel to the API.post I must use take(channel) and I want to handle this action in a different file so I dont have access to this channel. And I also not sure I want to export the channel and import it in a different file cause I want each saga to be independent.
What can I do?
One possibility is to create a wrapper function for API calls. Like this:
// `apiRequest` dispatches `anotherAction` and calls given `apiCall`
function* apiRequest(apiCall, ...args) {
yield put(anotherAction())
yield call(apiCall, ...arg)
}
export function* login({ payload }) {
try {
const resp = yield apiRequest(API.post, 'api/1/login', {
email: payload.email,
password: payload.password,
})
yield put(AuthActions.loginSuccess(resp.token))
} catch (error) {
yield put(AuthActions.loginError(error))
}
}

How to chain async actions redux saga (fetch of XHR requests)

Is there some usual pattern to chain redux-saga async requests synchronously? Eg. 1 function loads user ID and the second call some API request using that ID. I will try to demonstrate (this code isn't solution, just demonstration)
function* laodUserSaga(action) {
try {
const res = yield apiGet('/user')
const onboardingData = yield res.json()
yield put.resolve(loadUserSuccess(camelizeKeys(onboardingData)))
} catch (error) {
yield put.resolve(loadUserError(error))
}
}
function* loadProfileDataByUserID(action) {
const state = yield select();
try {
const res = yield apiGet(`/user/${state.userID}user-profile`)
const onboardingData = yield res.json()
yield put.resolve(loadUserSuccess(camelizeKeys(onboardingData)))
} catch (error) {
yield put.resolve(loadUserError(error))
}
}
function* loadProfileWithDataSaga(aciton){
yield put(laodUserSaga)
yield put(loadProfileDataByUserID)
}
function* sagaConnect() {
yield all([
takeLatest(LOAD_USER, laodUserSaga),
takeLatest(LOAD_USER_DATA_BY_PROFILE_ID, loadProfileDataByUserID),
takeLatest(LOAD_USER_WITH_PROFILE, loadProfileWithDataSaga),
])
}
you see, such examples would be really useful in docs of every library, 1 real world example is often what I understand in seconds, am I the only one?
You don't have to start every saga using takeEvery/Latest helper.
You can just call sagas as any other function, e.g.:
function* loadProfileWithDataSaga() {
const user = yield call(loadUser)
const profileData = yield call(loadProfileDataByUserID, user.id)
}
You just need to write loadUser and loadProfileDataByUserID sagas so that they receive and return the right values. If you want to be still able to call them in other scenarios by dispatching actions I would create another sagas that would just wrap the loadUser/Profile functionality.

Resources