redux-saga - Server-Side rendering with 1 async dependent on another - reactjs

I am trying to get my app to Server-Side render via the END effect (details on https://github.com/redux-saga/redux-saga/issues/255, with explanations why this is so tricky).
My data relies on 2 async requests: getJwtToken -> (with token data) FetchItem -> now render.
Is this possible at all?
I have spent time looking at Channels (here https://redux-saga.js.org/docs/advanced/Channels.html) and could not get any variation to work.
My Saga looks something like this (LOAD_USER_PAGE is fired initially)
function* loadUserPage() {
yield put({type: 'JWT_REQUEST'})
const { response } = yield call(fetchJwtToken)
if (response) {
yield put({type: 'JWT_REQUEST_SUCCESS', payload: response})
}
}
function* fetchItem() {
console.log('NEVER GETS HERE')
}
function* watchLoadPage() {
yield takeLatest('LOAD_USER_PAGE', loadUserPage);
}
function* watchFetchItem() {
yield takeLatest('JWT_REQUEST_SUCCESS', fetchItem);
}
export default function* rootSaga() {
yield all([
fork(watchLoadPage)
fork(watchFetchItem)
])
}
I believe I understand why it doesn't work (due to END event fired terminating only those effects which have started, and since my 2nd effect will not be fired until my first is back, it is not included in runSaga().done promise.
By doesn't work I mean the action JWT_REQUEST_SUCCESS is fired and the runSaga.done promise runs. But my message in console.log is not fired.
I think its possible by having both requests in the same function, but I am trying to abstract the token auth part out.
Is it not possible in any way?
Really stuck.

EDIT:
The solution was to use channels as suggested in the comment on https://github.com/redux-saga/redux-saga/issues/255#issuecomment-323747994 and https://github.com/redux-saga/redux-saga/issues/255#issuecomment-334231073.
SSR for all :)

Related

Why takeLeading redux-sagas isn't working

I'm not quite sure why takeLeading isn't working for me (takeLeading is supposed to take the first call and ignore subsequent calls until the first is returned). It's calling the same call 3 separate times like a takeEvery with the same parameters from 3 separate components in their useEffect(() => {getApiWatcher(params)}, []) on mount hook. It appears those don't return before the second is called either so I know it's not 3 uniquely separate calls.
function getApi(params) {
console.log('GET CALL') // called 3 times in console and network tab
return Api.doCall(
`API/${params.number}/${params.type}`,
'GET'
);
}
function* getApiActionEffect(action) {
const { payload } = action;
try {
const response = yield call(getApi, payload);
yield put(getApiSuccess({ data: response.data, status: response.status }));
} catch (e) {
yield put(getApiError(e.response));
}
}
export function* getApiActionWatcher() {
yield takeLeading( // should make sure only first call is made and subsequent are ignored
GET_API_WATCHER,
getApiActionEffect
);
}
// action
export function getApiWatcher(payload) {
return { type: GET_API_WATCHER, payload };
}
// passed dispatch as props
const mapDispatchToProps = (dispatch) => bindActionCreators( { getApiWatcher, }, dispatch );
// root saga
export default function* rootSaga() {
yield all([... getApiActionWatcher(),...])
}
There is a lot more code involved so I'm not creating a sample jsfiddle, but ideas for what could potentially be going wrong are what I'm looking for! Might have over looked something.
Turns out there were duplicate imported functions in the root saga. For example:
// root saga
export default function* rootSaga() {
yield all([
... getApiActionWatcher(),...
... getApiActionWatcher(),...
])
}
Removing the duplicates solved the issue. It also removed other duplicate calls I wasn't working on.
If you want to handle every GET_API_WATCHER action then you better takeEvery redux-saga helper.
And docs says that task spawned with takeLeading blocks others tasks until it's done.
takeLeading is working as intended. The calls to getApi instantly returns.
The only way your code would work the way you want it to is if getApi returns a Promise. If getApi() were to return a Promise, the getApiActionEffect would block until the Promise resolved.

Is it idiomatic use of redux-saga to call a worker saga directly from a React component?

I am using redux-saga having spent some time on core concepts of generators, generators with promises, and redux-saga itself. What I want below is to understand what is idiomatic and recommended, and what isn't.
In one file I define my root saga, watcher saga, and one worker saga (fetchSupplierOrders).
import {
fetchSupplierOrders,
} from './supplierOrders';
import { takeLatest} from 'redux-saga/effects';
function* watchSupplierOrdersSagas() {
yield takeLatest('REQUEST_FETCH_SUPPLIER_ORDERS', fetchSupplierOrders);
}
export default function* rootSaga() {
yield all([watchSupplierOrdersSagas()]);
}
Here is the worker saga:
export function* fetchSupplierOrders() {
try {
const supplierOrders = yield call(getSupplierOrders); // API call is in getSupplierOrders
// normally here I would use redux-saga put to hit my redux reducers
yield supplierOrders.map(({ id }) => id)
} catch (error) {
yield put({ type: 'RECEIVE_ERROR_FETCH_SUPPLIER_ORDERS', error: error.message });
}
}
I have a React component that when I click a button, it executes the worker saga. What I am trying to do here is to not go through the redux-saga watcher saga at all. I will simply execute the generator function myself in the component, and iterate through it. Usually, I would go through the watcher saga, and it would call a worker saga that would generate side effects by modifying redux state.
However, what if I want to make a network request, but I don't want to save the result in redux, but in local component state? I want the component to somehow get the results from the worker saga directly.
Here is the click handler for the button in the React component:
const handleFetchSuppliers = event => {
const it = fetchSupplierOrders({ payload: event.target.value });
const res1 = await it.next().value;
console.log('res1', res1);
const res2 = it.next(res1);
console.log('res2', res2);
This code will not work, because in the worker saga I am using redux-saga's call function. If I remove the use of call, and call getSupplierOrders (an async function) directly, then the await works and all the correct values are console.logged.
Is it common to do this (executing a worker saga from a component to get the results of an API request)? But if I do it this way then I lose the benefit of using call (isn't this useful because it's easier to test?)
Before redux-saga I would simply dispatch a thunk using redux-thunk, which is basically using async/await all the way through.
Do people mix the use of redux-thunk and redux-saga? Is this frowned upon?
However, what if I want to make a network request, but I don't want to save the result in redux, but in local component state?
If redux is not involved, then redux-saga is not the right tool to use. Just use the normal react approach: make the api request (often in componentDidMount), then wait for that promise to complete (with .then or await), then call setState.
If you want to have multiple ways to do the fetch (both via a saga, and via a direct call), then you could put the fetch into a helper function (regular function, not generator). The component and the saga could then both make use of the helper function, each wrapping it with whatever extra work they need to do.
For example:
// helper
async function fetchStuff() {
const response = await fetch('some Url');
if (!response.ok) {
throw response.status;
}
const data = await response.json();
return data.map(({ id }) => id);
}
// In a saga...
function* watchFetch() {
yield takeLatest('doFetch', fetchStuffSaga);
}
function* fetchStuffSaga() {
try {
const data = yield call(fetchStuff);
yield put({ type: 'success', payload: data });
} catch (err) {
yield put({ type: 'error', payload: err });
}
}
// In a component that dispatches an action:
componentDidMount() {
this.props.dispatch({ type: 'doFetch' });
}
// In a component that doesn't want to dispatch an action:
async componentDidMount() {
try {
const data = await fetchStuff();
this.setState({ data });
} catch (err) {
this.setState({ error: err });
}
}
This code will not work, because in the worker saga I am using redux-saga's call function. If I remove the use of call, and call getSupplierOrders (an async function) directly, then the await works and all the correct values are console.logged.
Sagas are not meant for manual iteration. If you try to manually iterate through a saga, you either need to have specialized knowledge about exactly what the saga will yield in what order, or you basically need to re-implement redux-saga yourself. The former is brittle and tightly coupled, the latter is a waste of effort.
Is it common to do this (executing a worker saga from a component to get the results of an API request)?
No.
Do people mix the use of redux-thunk and redux-saga? Is this frowned upon?
They're both trying to handle the same kinds of things (asynchronous actions). Your codebase will be simpler if you use just one approach, then trying to mix and match both.

How to wait execution of a saga to finish in redux-saga?

I have the following scenario:
export function* addCircle(circleApi, { payload }) {
try {
const response = yield apply(
circleApi,
circleApi.addCircle,
[payload]
);
if (response.error_type) {
yield put(addCircleFailedAction(response.error));
} else {
yield put(addCircleSucceededAction(response));
}
} catch (err) {
console.error(err);
}
}
export function* addTender(tenderApi, { payload }) {
try {
// NOTE: I want this to finish before continuing with rest of saga below.
yield call(addCircleAction(payload.circlePayload));
// Rest of saga removed for brevity.
} catch (err) {
console.error(err);
}
}
So, basically addCircle is making an API call, and depending on its success I call the appropriate redux action. Now, inside another saga I call the action responsible for addCircle saga, and I want it to finish execution before I continue with the rest of the saga. I tried to use call, but it basically doesn't wait for the addCircle saga to finish executing. Is there any way to wait for it? I call addCircle from inside my components and I didn't have the need to wait it, but in this specific instance I have to call it inside the saga, so I really need to wait for it to finish execution, change the state of the app, so that I can use the updated state in the rest of addTender saga. Any ideas?
As per your code snippet, your addCircle saga will dispatch either addCircleFailedAction or addCircleSucceededAction action creators just before it finishes execution. So we will have to wait for those action in your addTender saga.
Basically, this is what you should do. I'm just guessing your action types based on action creator names.
yield call(addCircleAction(payload.circlePayload));
yield take([ADD_CIRCLE_FAILED_ACTION, ADD_CIRCLE_SUCCEEDED_ACTION]);
// Rest of the saga
There is one edge case though. You are not dispatching any action in the catch block of your addCircle saga. Maybe you can dispatch an action called addCircleExceptionAction inside catch block and wait for it along with the other actions like this:
yield take([ADD_CIRCLE_FAILED_ACTION, ADD_CIRCLE_SUCCEEDED_ACTION, ADD_CIRCLE_EXCEPTION_ACTION]);
If you are dispatching multiple actions that would trigger addRender then there is no guarantee that take(...) would actually wait for the action that resulted of the yield call.
export function* addCircle(circleApi, { payload }) {
try {
const response = yield apply(
circleApi,
circleApi.addCircle,
[payload]
);
if (response.error_type) {
yield put(addCircleFailedAction(response.error));
return response;
} else {
yield put(addCircleSucceededAction(response));
return response;
}
} catch (err) {
console.error(err);
return {err};
}
}
export function* addTender(tenderApi, { payload }) {
try {
//because addCircle saga is returning something you can re use it
// in other sagas.
const result = yield call(addCircle,circleAPI?,payload.circlePayload);
//check for result.error_type here
// Rest of saga removed for brevity.
} catch (err) {
console.error(err);
}
}
Your code and the accepted answer would result in an error because call does not take an action object as first argument (it does take a {context,fn} type object).
Dispatching an action and then listening to another action that may or may not have been a side effect of the action you just dispatched is bad design. You dispatch these actions asynchronously and there is no guarantee they all take the same time to complete or provide the side effect you are waiting for in the same order as they were started.

How to handle array of requests in redux saga

I am tring to upload multiple files from my react native app. It's giving Unexpected Token error on yield statement.
Is it possible to do yield inside a loop?
files.map((fileOb)=>{
const response=yield call(FileManager.uploadFile, fileOb)
yield put(Actions.fileUploaded(response))
})
Thanks,
Sorry for my bad English
In your example above, you're yielding inside the callback passed to files.map. It doesn't work because you can use yield only inside a Generator function.
To handle parallel requests, you can either yield arrays of effects
function* uploadFiles(files) {
const responses = yield files.map(fileOb => {
return call(FileManager.uploadFile, fileOb)
})
yield responses.map(response => {
return put(Actions.fileUploaded(response))
})
}
Note that in this case all calls must succeed in order to dispatch the actions. i.e. the actions will not be dispatched until all calls are resolved with success (otherwise the Saga will cancel the remaining calls and raise an error).
Another way (perhaps what you'd expect) is to have parallel sagas for each individual process (call -> put). For example
function* uploadFiles(files) {
yield files.map(file => call(uploadSingleFile, file))
}
function* uploadSingleFile(file) {
try {
const response = yield call(FileManager.uploadFile, file)
yield put(Actions.fileUploaded(response))
} catch(err) {
yield put(Actions.fileUploadedError(response))
}
}
In the later example, an upload action will be dispatched as soon as the corresponding call has returned. Also because we've surrounded each individual process with a try/catch block, any errors will be handled individually and won't cause the other upload processes to fail
This worked for me to pass multiple files to uploadSingleFile generator function.
function* uploadFiles(files) {
yield all(files.map(file => call(uploadSingleFile, file)));
}

Redux-saga calls action after cancel

I'm trying to implement React-boilerplate with redux-saga inside. So i'm trying to fetch some data from the server and then make a redirect to another page. The problem is that before redirecting saga makes second request to the server. I guess there is something wrong with cancelling it. Here is a part of my code:
export function* fetchData() {
...
console.log('fetched');
yield browserHistory.push('/another-page');
}
export function* fetchDataWatcher() {
while (yield take('FETCH_DATA')) {
yield call(fetchData);
}
}
export function* fetchDataRootSaga() {
const fetchWatcher = yield fork(fetchDataWatcher);
yield take(LOCATION_CHANGE);
yield cancel(fetchWatcher);
}
export default [
fetchDataRootSaga
]
So in this example i have two console logs, the second one appears before redirecting. How can i fix it?
And another question. Actually, i have more functions in this file. Should i create "rootSaga" for each of them or i can cancel them all in that fetchDataRootSaga()? I mean is it normal if i cancel sagas this way:
export function* fetchDataRootSaga() {
const watcherOne = yield fork(fetchDataOne);
const watcherTwo = yield fork(fetchDataTwo);
...
yield take(LOCATION_CHANGE);
yield cancel(watcherOne);
yield cancel(watcherTwo);
...
}
Thanks in advance!
P.S. I'm not sure if this code is best practices. It is inspired by this repository
Maybe start by adjusting your loop inside fetchDataWatcher to look a little more like this
export function* fetchDataWatcher() {
while (true) {
yield take('FETCH_DATA');
yield call(fetchData);
}
}
Also you can route better by doing something like this perhaps
import { push } from 'react-router-redux';
import { put } from 'redux-saga/effects';
export function* fetchData() {
...
console.log('fetched');
yield put(push('/another-page'));
}
Overall I would hesitate to put a route change and then altogether separately do a take on it, only if you wish to cancel on all location changes (but I assume that's what you're after :) )
This defeats the purpose of saga, which is to handle potentially long running async requests and returns. You could instead set a state in your redux store like so
export function* fetchData() {
...
console.log('fetched');
yield put(setRedirectState('/another-page'));
}
Then see if the redirect state is set in your container in ComponentWillUpdate and redirect accordingly to something like this
import { push } from 'react-router-redux';
dispatch(push(state.redirecturl))
I haven't tried this, but the experience I have with React-boilerplate, this is what I would try first.

Resources