How to get the new state with Redux-Saga? - reactjs

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.

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.

How to warn react component when redux saga put a success action?

I am using the redux action pattern (REQUEST, SUCCESS, FAILURE) along with redux saga. I made a watcher and worker saga just like that:
import axios from 'axios';
import { put, call, takeEvery } from 'redux-saga/effects';
import * as actionTypes from 'constants/actionTypes';
import * as actions from 'actions/candidates';
const { REQUEST } = actionTypes;
// each entity defines 3 creators { request, success, failure }
const { fetchCandidatesActionCreators, addCandidateActionCreators } = actions;
const getList = () => axios.get('/api/v1/candidate/');
// Watcher saga that spawns new tasks
function* watchRequestCandidates() {
yield takeEvery(actionTypes.CANDIDATES[REQUEST], fetchCandidatesAsync);
}
// Worker saga that performs the task
function* fetchCandidatesAsync() {
try {
const { data } = yield call(getList);
yield put(fetchCandidatesActionCreators.success(data.data));
} catch (error) {
yield put(fetchCandidatesActionCreators.failure(error));
}
}
const postCandidate = params => axios.post('/api/v1/candidate/', params).then(response => response.data).catch(error => { throw error.response || error.request || error; });
// Watcher saga that spawns new tasks
function* watchAddCandidate() {
yield takeEvery(actionTypes.ADD_CANDIDATE[REQUEST], AddCandidateAsync);
}
// Worker saga that performs the task
function* AddCandidateAsync({ payload }) {
try {
const result = yield call(postCandidate, payload);
yield put(addCandidateActionCreators.success(result.data));
} catch (error) {
yield put(addCandidateActionCreators.failure(error));
}
}
export default {
watchRequestCandidates,
fetchCandidatesAsync,
watchAddCandidate,
AddCandidateAsync,
};
My reducer has two flags: isLoading and success. Both flags change based on the request, success and failure actions.
The problem is that I want my component to render different things when the success action is put on the redux state. I want to warn the component every time a _success action happens!
The flags that I have work well on the first time, but then I want them to reset when the component mounts or a user clicks a button because my component is a form, and I want the user to post many forms to the server.
What is the best practice for that?
The only thing I could think of was to create a _RESET action that would be called when the user clicks the button to fill up other form and when the component mounts, but I don't know if this is a good practice.
You need to assign a higher order component, also called a Container, that connects the store with your component. When usgin a selector, your component will automatically update if that part of the state changes and passes that part of the state as a prop to your component. (as defined in dspatchstateToProps)
Down below i have a Exmaple component that select status from the redux state, and passes it as prop for Exmaple.
in example i can render different div elements with text based on the status shown in my store.
Good luck!
import { connect } from 'react-redux'
const ExampleComponent = ({ status }) => {
return (
<div>
{status === 'SUCCESS' ? (<div>yaay</div>) : (<div>oh no...</div>)}
</div>
)
}
const mapStateToProps = state => {
return {
status: state.status
}
}
const mapDispatchToProps = dispatch => {
return {}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ExampleComponent)

Redux saga how to stop action from further propagating

I'm new to Redux Saga. I want to stop an action from further propagating. I am implementing row-level auto-saving mechanism. I use saga to detect row switch action, and then submit row changes and insert current row change action. codes like this:
// action-types.js
export const
SWITCH_ROW='SW_ROW',
CHANGE_CUR_ROW='CHG_CUR_ROW';
// actions.js
import {SWITCH_ROW,CHANGE_CUR_ROW} from './action-types'
export const switchRow=(oldRow,newRow)=>({type:SWITCH_ROW,oldRow,newRow})
export const changeRow=(row)=>({type:CHANGE_CUR_ROW,row})
// component.js
class MyComponent extends Component{
switchRow=(row)=>{
var oldRow=this.props.curRow;
this.props.dispatc(switchRow(oldRow,row));
}
render(){
...
{/* click on row */}
<div onClick={()=>this.switchRow(row)}>...</div>
...
}
}
// sagas.js
import {SWITCH_ROW} from './action-types'
import {changeRow} from './actions'
function* switchRow({oldRow,newRow}){
// Here I want to stop propagating SWITCH_ROW action further
// because this action is only designed to give saga a intervention
// point but not to be handled in reducer. I want a statement like
// below:
// stopPropogate();
if(oldRow && oldRow.modified===true){
yield call(svc.submit, oldRow);
}
yield put(changeRow(newRow))
}
export default function*(){
yield takeEvery(SWITCH_ROW,switchRow)
}
I know I can just ignore the SWITCH_ROW action in reducer. But, I think it's better if there is as least round trip as possible in program. Any suggests?
After more reading about Redux middle-ware, I think it's better to use a middle-ware to approach this goal.
At first, I renamed all type names of special actions for saga making them all starting with SAGA_. And then I use a Redux middle-ware to identify them and swallow them, and then those special actions can't reach reducer any more. Here is the codes:
// glutton.js
const glutton = () => next => action => {
if (!action.type.startsWith('SAGA_')) return next(action);
}
// store.js
...
const store = createStore(rootReducer, applyMiddleware(sagaMiddleWare, glutton, logger));
Lets say you are in a file which is not redux component, i mean you don't have access to dispatch , so i think throwing an error would be nice, and here is the code to catch the exception anywhere:
export default function autoRestart(generator) {
return function* autoRestarting(...args) {
while (true) {
try {
yield call(generator, ...args);
} catch (e) {
console.log(e);
yield put({ type: ReduxStates.Error });
}
}
}
};
all your sagas, need to implement this base function.

Redux Saga action crashes app

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().

Promises in redux-saga

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

Resources