I have a networkSaga, where I fetch posts, add and remove likes. After I added or removed a like, I need to call getPosts to update the number of likes.
In redux-thunk, I'll simply call dispatch(getPosts()) after I added or removed the like. Since, I'm new to sagas, I'm concerned, how should it be done?
import { all, call, fork, put, takeEvery } from 'redux-saga/effects';
import { NetworkActionTypes } from './types';
import { apiCaller } from '../../utils/apiCaller';
import { onSuccess, onFailure } from '../../utils/actionCreators';
function* getPosts(action): Generator {
try {
const res = yield call(apiCaller, action.meta.method, action.meta.route, action.meta.data);
yield put(onSuccess(NetworkActionTypes.GET_POSTS_SUCCESS, res));
} catch (err) {
yield put(onFailure(NetworkActionTypes.GET_POSTS_ERROR, err));
}
}
function* addLike(action): Generator {
try {
const res = yield call(apiCaller, action.meta.method, action.meta.route, action.meta.data);
yield put(onSuccess(NetworkActionTypes.ADD_LIKE_SUCCESS, res));
} catch (err) {
yield put(onFailure(NetworkActionTypes.ADD_LIKE_ERROR, err));
}
}
function* removeLike(action): Generator {
try {
const res = yield call(apiCaller, action.meta.method, action.meta.route, action.meta.data);
yield put(onSuccess(NetworkActionTypes.REMOVE_LIKE_SUCCESS, res));
} catch (err) {
yield put(onFailure(NetworkActionTypes.REMOVE_LIKE_ERROR, err));
}
}
/**
* #desc Watches every specified action and runs effect method and passes action args to it
*/
function* watchFetchRequest(): Generator {
yield takeEvery(NetworkActionTypes.GET_POSTS, getPosts);
yield takeEvery(NetworkActionTypes.ADD_LIKE, addLike);
yield takeEvery(NetworkActionTypes.REMOVE_LIKE, removeLike);
}
/**
* #desc saga init, forks in effects, other sagas
*/
export function* networkSaga() {
yield all([fork(watchFetchRequest)]);
}
The question may sound foolish, but I'll be grateful if you point me to the solution. Thanks!
you can do by yield getPosts(action json you have to pass here) once you receive the success from add/remove api.
But i suggest better to trigger a action from the component which does that for you,
This will help you to keep all the generator functions re-usable and testable.
Related
I'm currently converting the sagas in the code base to thunks.
I know that Sagas' specific functions such yield put, yield call have a "direct translation" to thunks dispatch(...) and await fn....
I came across yield take which from what I understand takes a set of actions included in the store and instructs the middleware to wait for one of those specified actions from the store and the result is an action object that gets dispatched?
What would be the "equivalent" if using Redux thunks?
Many thanks!
sagas example:
export function* sample() {
try {
const response = yield call(api.sample)
yield put(setData(response.data))
} catch (error) {
yield put(setError(error))
}
}
export function* sampleSaga() {
yield takeEvery( YOUR_TYPE, sample)
}
if you wanna change it to redux thunk , you can do it:
export function sample(){
return (dispatch, getState) => {
api.sample()
.then((response)=> dispatch(setData(response.data)))
.catch(error => Promise.reject(error))
}
}
I'm trying to get the new state to which the getCart() generator function returns me in reducer, but the state is coming "late".
The state I need comes only after the second click of the button.
NOTE: The error on the console I am forcing is an action.
import { call, put, select, all, takeLatest } from 'redux-saga/effects';
import { TYPES } from './reducer';
import { getCart, getCartSuccess, getCartFail } from './actions';
import API from 'services/JsonServerAPI';
export function* getCartList() {
try {
const response = yield call(API.get, '/2cart');
yield put(getCartSuccess(response.data));
} catch (error) {
yield put(
getCartFail(error.response ? error.response.statusText : error.message)
);
}
}
export function* addToCart({ id }) {
yield put(getCart());
yield select(({ CartReducer }) => {
console.log(CartReducer);
});
console.log(id);
}
// prettier-ignore
export default all([
takeLatest(TYPES.GET, getCartList),
takeLatest(TYPES.ADD, addToCart)
]);
Since getCartList performs async actions you will need some way to wait for those to complete in the addToCart before logging.
One option is to call the getCartList directly from the addToCart saga without dispatching a redux action - this may not be preferable if you have other middleware that relies on TYPES.GET being dispatched.
export function* addToCart({ id }) {
// call the `getCartList` saga directly and wait for it to finish before continuing
yield call(getCartList);
yield select(({ CartReducer }) => {
console.log(CartReducer);
});
console.log(id);
}
The other option is take on the list of actions that will be dispatched once the getCartList saga completes:
export function* addToCart({ id }) {
yield put(getCart());
// wait until one of the success or failure action is dispatched, sub with the proper types
yield take([TYPES.GET_SUCCESS, TYPES.GET_FAILURE]);
yield select(({ CartReducer }) => {
console.log(CartReducer);
});
console.log(id);
}
This has some potential tradeoffs as well - you will need to make sure the action list in take stays up to date with all possible ending types that getCartList can put and you need to make sure you keep using takeLatest (vs say takeEvery) to trigger addToCart so you don't end up with multiple concurrent sagas that could fulfill the take clause.
I have a simple React / Redux / Redux Sagas app which uses an API to show a random picture of a dog upon clicking a button.
dogSagas.js
import { put, call, takeEvery, all } from 'redux-saga/effects';
import * as types from '../actions/types';
import * as actions from '../actions/dogActions';
import { DOG_API } from '../constants/variables';
function* getDogAsync() {
try {
yield put(actions.getDog);
const data = yield call(() =>
fetch(DOG_API)
.then(res => res.json())
.catch(err => console.error(err)),
);
yield put(actions.getDogOk(data));
} catch (error) {
yield put(actions.getDogFail());
}
}
function* watchGetDog() {
yield takeEvery(types.GET_DOG, getDogAsync);
}
export default function* rootSaga() {
yield all([watchGetDog()]);
}
dogActions.js
import * as types from '../actions/types';
export const getDog = () => ({
type: types.GET_DOG,
});
export const getDogOk = data => ({
type: types.GET_DOG_OK,
payload: data.message,
});
export const getDogFail = () => ({
type: types.GET_DOG_FAIL,
});
errors
However, I have two different errors.
1.) When I do yield put(actions.getDog); the app works, but in the console I get the error:
uncaught at getDogAsync Error: Actions must be plain objects. Use custom middleware for async actions.
2.) If instead I do: yield put(actions.getDog()); the app consumes a lot of CPU and effectively crashes.
questions
So, my questions are:
1.) Why does this approach cause Redux to complain about non-plain objects?
2.) Why does this seemingly innocuous statement cause the app to crash?
full code
Full code on StackBlitz here.
The issue was that I was calling the getDog() action creator within the async generator getDogAsync(). Since we had a watcher, this was leading to an infinite number of calls to getDog().
So, to fix, just remove:
yield put(actions.getDog);
within the getDogAsync().
I'm combining a set of Sagas, ( some takeEvery and takeLatest ) from different modules and using yield all(sagas) to combine them all in RootSaga.
Everything works, without issue. I catch errors inside the Sagas themselves. But, now the requirement is to catch errors at RootSaga level too, in any case, someone misses catching a problematic part.( Actually, I'm working on boilerplate for a multi-team project. )
I see, if someone is not using try catch to catch a problematic part, then the error is propagated and Saga completely stops working after that. Sagas won't watch anything thereafter.
What I want to do is, let other Sagas run without issues and RootSaga to be working thereafter, keep watching as usual. How can I achieve this?
export default function* rootSaga() {
yield all(sagas);
}
For v1 use
rootSagaTask.toPromise().catch(e => {
});
When you run the rootSaga you are getting back a task. That task has a done property which is a promise. So:
const rootSagaTask = reduxSagaMiddleware.run(rootSaga);
rootSagaTask.done.catch(error => {
// Error here is a fatal error.
// None of the sagas down the road caught it.
});
After much thinking about this problem, I implemented my own middleware function. Like:
Middleware function:
* #function errorFallback
* #param {CallableFunction} sagaFunction
* #param {object} action
* #description
* * Captured exceptions which is occurred from saga function.
* * Whenever any exception occurred from saga functions, whole sagas will be destroyed.
* * We can use this function to save sagas from die.
* #example
* export function* getSagaData() {
* yield all([
* takeLatest('GET_SAGA_DATA', errorFallback, getSaga),
* ]);
* }
*/
function* errorFallback(sagaFunction, action) {
try {
yield sagaFunction(action);
} catch (error) {
// exception log to your server
}
}
export default errorFallback;
Uses
export function* loadSampleSaga() {
try {
const response = yield post('/create', {
name: 'test',
salary: '123',
age: '23',
});
yield put(sampleSuccessAction());
} catch (err) {
yield put(sampleFailureAction());
throw err;
}
}
export function* getSampleSaga() {
yield all([takeLatest('GET_SAMPLE_SAGA_DATA', errorFallback, loadSampleSaga)]);
}
I found the same question here, but without a proper answer I am looking for.
I am developing a simple application with CRUD operations. On the edit page, after the component gets mounted (componentDidMount()), the app dispatches an action to retrieve a specific post details:
dispatch({ type: FETCH_POST, id: 'post-id' })
I am using redux-saga and want the above call to return a Promise so that I can access the API response.
Right now, without a callback/Promise, I ended up with defining a new state in store (like post_edited) and connect/map it to props in the component for edit page.
What would be the best possible way to deal with this kind of situation?
Could you please provide more information about your issue? I'm not sure if I understand your issue properly, but the common practice is:
API.js
function apiCallToFetchPost(id) {
return Promise.resolve({name: 'Test});
}
postSaga.js
function* fetchPostSaga({id}) {
try {
const request = yield call(apiCallToFetchPost, id);
// -> in post reducer we will save the fetched data for showing them later
yield put({type: FETCH_POST_SUCCESS, payload: request});
} catch (error) {
yield put({type: FETCH_POST_SUCCESS_FAILURE, error})
}
}
export function* onBootstrap() {
yield takeLatest(FETCH_POST, fetchPostSaga);
}
There's a package that does exactly what the OP requested, i.e. arranges that dispatch() can return a promise: #adobe/redux-saga-promise
Using it, you define a "promise action" creator via:
import { createPromiseAction } from '#adobe/redux-saga-promise'
export const fetchPostAction = createPromiseAction('FETCH_POST')
The dispatch() of a "promise action" will return a promise:
await dispatch(fetchPostAction({ id: 'post-id' }))
The saga might look like:
import { call, takeEvery } from 'redux-saga/effects'
import { implementPromiseAction } from '#adobe/redux-saga-promise'
import { fetchPostAction } from './actions'
function * fetchPostSaga(action) {
yield call(implementPromiseAction, action, function * () {
const { id } = action.payload
return yield call(apiCallToFetchPost, id)
})
}
export function * rootSaga() {
yield takeEvery(fetchPostAction, fetchPostSaga);
}
It will resolve the promise with the value returned by apiCallToFetchPost or reject if apiCallToFetchPost throws an error. It also dispatches secondary actions with the resolution/rejection that you can access in a reducer. The package provides middleware you have to install to make it work.
(Disclaimer, I'm the author)
I am the developer of #teroneko/redux-saga-promise. It was initially forked from #adobe/redux-saga-promise but now it has been completelly revamped to use createAction from #reduxjs/toolkit to support TypeScript.
To keep in touch with the example of #ronen, here the TypeScript equivalent.
Create promise action (creator):
import { promiseActionFactory } from '#teroneko/redux-saga-promise'
export const fetchPostAction = promiseActionFactory<void>().create<{ id: string }>('FETCH_POST')
To dispatch a promise action (from creator):
// promiseMiddleware is required and must be placed before sagaMiddleware!
const store = createStore(rootReducer, {}, compose(applyMiddleware(promiseMiddleware, sagaMiddleware)))
await store.dispatch(fetchPostAction({ id: 'post-id' }))
To resolve/reject the promise action (from saga):
import { call, takeEvery } from 'redux-saga/effects'
import { implementPromiseAction } from '#teroneko/redux-saga-promise'
import { fetchPostAction } from './actions'
function * fetchPostSaga(action: typeof fetchPostAction.types.triggerAction) {
yield call(implementPromiseAction, action, function * () {
const { id } = action.payload
return yield call(apiCallToFetchPost, id)
})
// or for better TypeScript-support
yield call(fetchPostAction.sagas.implement, action, function * () {
const { id } = action.payload
return yield call(apiCallToFetchPost, id)
})
}
export function * rootSaga() {
yield takeEvery(fetchPostAction, fetchPostSaga);
}
So what's going on?
promise action (creator) gets created
promise action (from creator) gets created and
dispatched to store.
Then the promise action gets converted to a awaitable promise action where its deferred version is saved into the meta property. The action is immediatelly returned and
passed to saga middleware.
The now awaitable promise action is qualified to be used in implementPromiseAction that nothing else does than resolving or rejecting the deferred promise that is saved inside the meta property of the awaitable promise action.
See README for more features and advanced use cases.
Another solution
onSubmit: (values) => {
return new Promise((resolve, reject) => {
dispatch(someActionCreator({ values, resolve, reject }))
});
}
In saga:
function* saga() {
while (true) {
const { payload: { values, resolve, reject } } = yield take(TYPE)
// use resolve() or reject() here
}
}
Reference: https://github.com/redux-saga/redux-saga/issues/161#issuecomment-191312502