I am using Redux for state management and saga as a middleware. For some reason my app is in some infinite loop state of calling API endpoint.
This is my actions:
export const GET_USERS = "GET_USERS";
export const getUsers = () => ({
type: GET_USERS,
});
export const GET_USERS_SUCCESS = `${GET_USERS}_SUCCESS`;
export const getUsersSuccess = (data) => ({
type: GET_USERS_SUCCESS,
payload: data,
});
export const GET_USERS_FAIL = `${GET_USERS}_FAIL`;
export const getUsersFail = (error) => ({
type: GET_USERS_FAIL,
payload: error,
});
This is saga:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.getUsers, getUsers$)]);
}
This is a reducer:
export default (state = initialState(), action) => {
const { type, payload } = action;
switch (type) {
case actions.GET_USERS:
return {
...state,
users: {
...state.users,
inProgress: true,
},
};
case actions.GET_USERS_SUCCESS:
return {
...state,
users: {
inProgress: false,
data: payload,
},
};
case actions.GET_USERS_FAIL:
return {
...state,
users: {
...state.users,
inProgress: false,
error: payload,
},
};
default:
return state;
}
};
And this is a component connected with redux:
const Home = (props) => {
useEffect(() => {
props.getUsers();
console.log('props', props.data);
}, []);
return(
<h1>Title</h1>
);
}
const mapStateToProps = ({
users: {
users: {
data
}
}
}) => ({data})
export default connect(mapStateToProps, {getUsers})(Home);
Why is this happening?
This is due to the fact that you misused the sagas in your example. As with any other effect creator as the first parameter must pass a pattern, which can be read in more detail in the documentation. The first parameter can also be passed a function, but in a slightly different way. View documentation (block take(pattern)).
In your case, you are passing a function there that will return an object
{
type: 'SOME_TYPE',
payload: 'some_payload',
}
Because of this, your worker will react to ALL events that you dispatch.
As a result, you receive data from the server, dispatch a new action to save data from the store. And besides the reducer, your getUsers saga will be called for this action too. And so on ad infinitum.
Solution
To solve this problem, just use the string constant actions.GET_USERS that you defined in your actions.
And your sagas will look like this:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.GET_USERS, getUsers$)]);
}
This should fix your problem.
Related
I have a Next.js application in which I use redux / redux saga.
Although I receive data from the backend (so there is a payload that I can see in the browsers network tab), it is sent to my reducer as undefined. I thought that it might be due to the payload type and set it to any. Still, it doesn't want to work.
Here is my setup (shown in simplified form):
actions.ts
export const getSomeStuff: ActionType<any> = (data: any) => ({
type: actionTypes.GET_SOME_STUFF,
payload: data,
});
export const getSomeStuffSuccess: ActionType<any> = (data: any) => ({
type: actionTypes.GET_SOME_STUFF_SUCCESS,
payload: data,
});
actionTypes.ts
export const actionTypes = {
GET_SOME_STUFF: 'GET_SOME_STUFF',
GET_SOME_STUFF_SUCCESS: 'GET_SOME_STUFF_SUCCESS',
};
reducers.ts
interface customInterface {
list: any;
}
export const initialState: customInterface = {
list: null,
};
function reducer(state = initialState, action: ReturnType<ActionType>) {
switch (action.type) {
case actionTypes.GET_SOME_STUFF_SUCCESS:
return {
list: action.payload,
};
default:
return state;
}
}
sagas.ts
function* getSomeStuffSaga(action: ReturnType<ActionType>) {
try {
const payload: any = yield call(api.getSomeStuff, action.payload);
yield put(getSomeStuffSuccess(payload));
} catch (error) {
console.error(error);
}
}
function* watchGetSomeStuffSaga() {
yield takeLatest(actionTypes.GET_SOME_STUFF, getSomeStuffSaga);
}
export default function* sagas() {
yield fork(watchGetSomeStuffSaga);
}
The api call
getSomeStuff = (data: any) => {
http.get(`my/custom/endpoint`) as Promise<any>;
};
The dispatch
dispatch(getSomeStuff('someStringParametersPassed'));
Simplified payload I get (in the browsers network tab)
{
"items": [
{
"id":"123456789",
"stuff":{
"generalStuff": {
...
},
"basicStuff": {
...
}
}
},
]
}
I guess you simply forgot to return in the api call:
getSomeStuff = (data: any) => {
return http.get(`my/custom/endpoint`) as Promise<any>;
};
:)
I'm trying to get data in nextjs with redux-saga. But my data wont show, then i debug in every file, and finally found that the data is successfully fetched but, the state wont updated. I've been follow the docs and dont know which part that caused the issue. Thanks for any help
Default State
{
status: 'init',
data: [],
error: null,
}
Saga
export function* getCars() {
try {
const options = {
url: `http://localhost/some-endpoint`,
method: 'GET',
}
yield put({
type: ACTION_TYPES.CARS.LOAD,
});
const response = call(axios, options);
if (response.data) {
const { data, status } = response;
yield put({
type: ACTION_TYPES.CARS.RES,
payload: { data, status },
});
} else {
yield put({
type: ACTION_TYPES.CARS.ERR,
error: response.status,
});
}
} catch (err) {
yield put({
type: ACTION_TYPES.CARS.ERR,
error: err,
});
}
}
export default function* combineSaga() {
yield takeEvery(ACTION_TYPES.CARS.GET, getCars);
}
Reducer
const defaultState = DEFAULT_STATE.CARS;
function reducer(state = defaultState, action) {
switch (action.type) {
case ACTION_TYPES.CARS.LOAD:
return {
...state,
status: 'loading',
};
case ACTION_TYPES.CARS.RES:
return {
...state,
data: action.payload.data,
status: 'success',
};
case ACTION_TYPES.CARS.ERR:
return {
...state,
status: 'error',
error: action.error,
};
default:
return state;
}
}
export default reducer;
Store Configuration
const makeStore = () => {
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware),
)
store.sagaTask = sagaMiddleware.run(rootSaga)
return store
}
export default createWrapper(makeStore);
App.js
function MyApp({ Component, pageProps }) {
return (
<div>
<Component {...pageProps} />
</div>
);
}
MyApp.getInitialProps = async appContext => {
const { Component, ctx } = appContext;
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { pageProps };
}
export default wrapper.withRedux(withReduxSaga(MyApp));
Index.js
function Home({ storeCars }) {
useEffect(() => {
//always show default state
console.log(storeCars);
}, [storeCars.status]);
return <div>Home</div>;
}
Home.getInitialProps = async props => {
const { store } = props;
store.dispatch({
type: ACTION_TYPES.CARS.GET,
payload: {},
});
return {};
}
export default connect(state => state)(Home);
you need 2 actions
getDataFromServer to be used in Home component
setDataToRedux to be used in saga and action type in reducer
enter code hereI'm providing Redux Global State to my whole react app through a Provider wrapper in my app.js file.
I've no problem accessing any other piece of state other than "Current Profile".
Here is the component:
import React, { Fragment, useEffect } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { loadTargetProfiles, loadCurrentProfile } from "../../actions/profile";
const Friends = ({
loadCurrentProfile,
loadTargetProfiles,
profile: { currentProfile, targetProfiles, targetProfilesAreLoading },
}) => {
useEffect(() => {
loadTargetProfiles();
loadCurrentProfile();
}, []);
console.log(currentProfile);
return (
...
};
Friends.propTypes = {
loadTargetProfiles: PropTypes.func.isRequired,
loadCurrentProfile: PropTypes.func.isRequired,
profile: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
profile: state.profile,
});
export default connect(mapStateToProps, {
loadCurrentProfile,
loadTargetProfiles,
})(Friends);
here is the loadCurrentProfile action responsible for providing the currentProfile piece of state.
export const loadCurrentProfile = () => async (dispatch) => {
try {
const res = await api.get("/profile/current");
dispatch({
type: LOAD_CURRENT_PROFILE,
payload: res.data,
});
} catch (err) {
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status },
});
}
};
here is the relevant part of the Reducer
const initialState = {
currentProfile: null,
targetProfile: null,
targetProfiles: [],
currentProfileIsLoading: true,
targetProfileIsLoading: true,
targetProfilesAreLoading: true,
error: {},
};
//
// Export Reducer
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
case LOAD_CURRENT_PROFILE:
return {
...state,
currentProfile: payload,
currentProfileIsLoading: false,
};
here is the API that's getting hit:
router.get("/current", auth, async (req, res) => {
try {
const profile = await Profile.findOne({
user: req.user.id,
}).populate("user", ["_id", "username", "registerdate"]);
if (!profile) {
return res
.status(400)
.json({ msg: "There is no profile for this user." });
}
res.json(profile);
} catch (err) {
console.error(err.message);
res.status(500).send("Server Error...");
}
});
here is the console
here is the currentProfile piece of state expanded:
when i try to reach into the currentProfile piece of state, for example
const Friends = ({
loadCurrentProfile,
loadTargetProfiles,
profile: { currentProfile: {
avatar
}, targetProfiles, targetProfilesAreLoading },
}) => {
useEffect(() => {
loadTargetProfiles();
loadCurrentProfile();
}, []);
console.log(avatar);
It give me the error:
TypeError: Cannot read property 'avatar' of null
here is Redux Dev Tools screenshot:
Fix:
I (temporarily) got rid of the error by accessing the inner state after declaring the function.
const Friends= ({
profile: { currentProfileIsLoading },
currentProfile,
}) => {
currentProfile && console.log(currentProfile.avatar);
and it works for now but it certainly isn't the most elegant solution. Is there a way to add this guard in the function declaration in order to set the state in one place?
Issue: Both targetProfiles and targetProfilesAreLoading are truthy values in your state, but currentProfile is null until the GET resolves. You can't access the avatar property of a null object.
You can provide some default argument value for profile, this only works really though if profile is undefined, null counts as a defined value.
const Friends = ({
loadCurrentProfile,
loadTargetProfiles,
profile: {
currentProfile = {},
targetProfiles,
targetProfilesAreLoading
},
}) => {
useEffect(() => {
loadTargetProfiles();
loadCurrentProfile();
}, []);
console.log(currentProfile);
return (
...
};
You can also use a guard on the possibly undefined/null object
currentProfile && currentProfile.avatar
Another alternative is to use a state selector library like reselect that allows you to pull/augment/derive/etc... state values that get passed as props. This also allows you to set default/fallback values for state. It pairs with redux nicely.
I am trying to dispatch functions from reducer but it call only one function.
Reducer looks like this:
import types from "./types";
const initState = {
active: false,
myData: []
};
function toggleActive(state, action) {
return {
...state,
active: action.payload
};
}
function watchInfo(state, action) {
return {
...state,
myData: action.payload
};
}
const watchReducer = (state = initState, action) => {
switch (action.type) {
case types.TOGGLE_ACTIVE:
return toggleActive(state, action);
case types.WATCH_DATA:
return watchInfo(state, action);
default:
return state;
}
};
export default watchReducer;
and action creator is set like this:
import types from "./types";
function toggleActive(bool) {
return {
type: types.TOGGLE_ACTIVE,
payload: bool
};
}
function watchInfo(data) {
return dispatch => {
dispatch({
type: types.WATCH_DATA,
payload: data
});
};
}
export { toggleActive as default, watchInfo };
and in component in which I am importing connect and corresponding action creator, i am trying to use it like this:
const mapStateToProps = state => {
const mapDispatchToProps = dispatch => ({
watchInfo: () => dispatch(watchInfo())
});
export default connect
mapDispatchToProps
)(MyComponent);
So when I inspect in redux console it only calls toggleActive, never calls watch info.
I am not sure what I am doing wrong.
change this action creator
function watchInfo(data) {
return dispatch => {
dispatch({
type: types.WATCH_DATA,
payload: data
});
};
}
to:
function watchInfo(data) {
return {
type: types.WATCH_DATA,
payload: data
}
}
action creator is a function that return an object that representing an action. we use action creators for better code maintenance and prevent some Spelling error but this code:
dispatch(watchInfo(someData))
is equivalent to this:
dispatch({
type: types.WATCH_DATA,
payload: someData
})
Until now I've been using redux-thunk for async actions. On application startup I use to have to load some data from some server. So what I do is to create async actions and then use async/await in order to know when they finished. While async actions are fetching I render a splashscreen. When they finish then I start the application.
Now I'm switching to redux sagas and I don't know how to do it with them. I cannot use async/await. What I thought is to have a boolean var in every object of the store that needs to fetch data. However I would like to know if there is any pattern to manage it in a clean way. Does anybody know any pattern for this purpose?
// example with thunks
import { someAsyncAction, someAsyncAction2 } from './actions';
const initialDispatches = async (store) => {
await store.dispatch(someAsyncAction());
await store.dispatch(someAsyncAction2());
};
export default initialDispatches;
In my opinion there's no right/wrong pattern in this kind of cases.
Iv'e put up an example for you of how your goal could be achieved using saga.
The basic idea: have a separate saga for each resource (for instance, I used to split into feature sagas), and a saga for the initialization.
Then the main root saga will run them all parallelly, and you will be able to trigger the initialization saga somewhere in your app and let it all happen:
Note: this example is super naive and simple, you should find a better way for organizing everything up, I just tried to keep it simple.
const {Provider, connect} = ReactRedux;
const {createStore, applyMiddleware} = Redux;
const createSagaMiddleware = ReduxSaga.default;
const {takeEvery, takeLatest} = ReduxSaga;
const {put, call, all, fork} = ReduxSaga.effects;
const initialState = {
fruits: [],
vegtables: []
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_FRUITS':
return {
...state,
fruits: [
...action.payload.fruits
]
}
case 'SET_VEGTABLES':
return {
...state,
vegtables: [
...action.payload.vegtables
]
}
}
return state;
};
//====== VEGTABLES ====== //
async function fetchVegtables() {
return await new Promise((res) => {
setTimeout(() => res([
'Cuecumber',
'Carrot',
'LEttuce'
]), 3000)
});
}
function* getVegtables() {
const vegtables = yield call(fetchVegtables);
yield put({ type: 'SET_VEGTABLES', payload: { vegtables } })
}
function* vegtablesSaga() {
yield takeEvery('GET_VEGTABLES', getVegtables);
}
//====== VEGTABLES ====== //
//====== FRUITS ====== //
async function fetchFruits() {
return await new Promise((res) => {
setTimeout(() => res([
'Banana',
'Apple',
'Peach'
]), 2000)
});
}
function* getFruits() {
const fruits = yield call(fetchFruits);
console.log(fruits)
yield put({ type: 'SET_FRUITS', payload: { fruits } })
}
function* fruitsSaga() {
yield takeEvery('GET_FRUITS', getFruits);
}
//====== FRUITS ====== //
//====== INIT ====== //
function* initData() {
yield all([
put({ type: 'GET_FRUITS' }),
put({ type: 'GET_VEGTABLES' })
]);
}
function* initSaga() {
yield takeLatest('INIT', initData);
}
//====== INIT ====== //
// Sagas
function* rootSaga() {
yield all([
yield fork(initSaga),
yield fork(fruitsSaga),
yield fork(vegtablesSaga),
]);
}
// Component
class App extends React.Component {
componentDidMount() {
this.props.dispatch({ type: 'INIT' });
}
render () {
return (
<div>
<div>fruits: {this.props.fruits.join()}</div>
<div>vegtables: {this.props.vegtables.join()}</div>
</div>
);
}
}
// Store
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);
const ConnectedApp = connect((state) => ({
fruits: state.fruits,
vegtables: state.vegtables
}))(App);
// Container component
ReactDOM.render(
<Provider store={store}>
<ConnectedApp />
</Provider>,
document.getElementById('root')
);
As you can see, I have two resources: fruits and vegetables.
Each resource has it's own saga, which is responsible for watching for GET actions dispatched somewhere.
Each of them using basic saga effects such as call, put etc to fetch the resources asyncly, and then they dispatch it to the store (and then the reducer handles them).
In addition, Iv'e set up an initSaga which uses the all effect to trigger all of the resource fetching sagas in a parallel way.
You can see the whole example running here:
https://jsfiddle.net/kadoshms/xwepoh5u/17/
I wrote about creating a structure on top of redux-saga to facilitate async operations by providing an initial action and then loading/success/error states based on the result of the operation. It's in 2 parts, first sync and then async.
It basically lets you write your reducers declaratively, like an object. You only have to call the initial action and the saga takes care of the rest and your UI can respond to the results when loading/success/error actions are triggered. Below is what the reducer looks like.
const counterAsync = {
initialState: {
incrementAsync_result: null,
incrementAsync_loading: false,
incrementAsync_success: false,
incrementAsync_error: false,
},
incrementAsync: {
asyncOperation: incrementAPI,
action: ({number}) => {
type: ACTION_INCREMENT_ASYNC,
payload: {
number: number
}
}
loading: {
action: (payload) => {
return {
type: ACTION_INCREMENT_ASYNC,
payload: { ...payload }
}
},
reducer: (state, action) => {
state.incrementAsync_loading = true
state.incrementAsync_success = false
state.incrementAsync_error = false
}
},
success: {
action: (payload) => {
return {
type: ACTION_INCREMENT_ASYNC,
payload: { ...payload }
}
},
reducer: (state, action) => {
state.incrementAsync_result = action.payload
state.incrementAsync_loading = false
state.incrementAsync_success = true
state.incrementAsync_error = false
}
},
fail: {
action: (payload) => {
return {
type: ACTION_INCREMENT_ASYNC,
payload: { ...payload }
}
},
reducer: (state, action) => {
state.incrementAsync_result = action.payload
state.incrementAsync_loading = false
state.incrementAsync_success = false
state.incrementAsync_error = true
}
}
},
}
We use a slightly more heavy weight version of this pattern at work and it's much better than vanilla redux/saga.
Let me know if you have any questions!
https://medium.com/#m.razajamil/declarative-redux-part-1-49a9c1b43805
https://medium.com/#m.razajamil/declarative-redux-part-2-a0ed084e4e31