Redux Saga action crashes app - reactjs

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

Related

Equivalent of a yield take Redux Saga in Redux thunks

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))
}
}

How to extract data from Axios get request?

I have two files. One is "actions.js" and the other one is "Profile.js". The first file has a function that calls an API that will get information about an user based on his/her id.
The code for "actions.js" is:
import axios from "axios";
export const getPerson = (id, history) => async dispatch => {
try {
const res = await axios.get(`http://localhost:6969/users/getUsers/${id}`);
const { email } = res.data;
console.log(email);
dispatch({
type: GET_PERSON,
payload: res.data,
});
} catch (error) {
history.push("/profile");
}
};
The code for my "Profile.js" page is:
import React, { Component } from 'react'
import { getPerson } from '../actions/personActions';
import * as PropTypes from 'prop-types'
import { connect } from "react-redux";
render() {
this.props.getPerson(id, this.props.history);
----------- Followed by render method
}
Profile.propTypes = {
getPerson: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
errors: state.errors
});
export default connect(
mapStateToProps,
{ getPerson }
)(Profile);
Problem is that I cannot show those responses in my Profile page by even localstorage if I decide to put the respective value in it. Tried with variety of ways but it shows up in the action.js page if I see from console using inspect log however it shows undefined in the Profile.js page. Please tell me where did I made it wrong. Thanks.
So, I think you're issue is that you're not dispatching anything in catch here:
} catch (error) {
dispatch({
// or something like this
type: GET_PERSON,
payload: error,
});
history.push("/profile");
}
Because of this, state is not updated with new error and state.errors is probably null or undefined.
Also, If you're trying to render anything other than the error from the state, don't forget to add it to mapStateToProps.
Other than that, I would recommend against calling api function in render. That's a really bad idea, that's because you might get a lot of useless api calls or even a loop of renders. Either call it in componentDidMount or use useEffect hook.
Generally I would advise that you go through couple of more react/redux tutorials.

How to get the new state with Redux-Saga?

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.

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)

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