Error: Invariant failed: A state mutation was detected between dispatches, in the path 'emailReducer.emails.0' - redux

I'm getting this error message about mutating state, but the purpose of Redux Toolkit is mutating state, so I'm confused...
The error is coming from addNewEmail, where I'm adding new emails to the array calling prevEmails using useSelector and the second parameter is a regular string.
import { createSlice } from "#reduxjs/toolkit";
import { AppThunk } from "./store";
const initialState = {
emails: [],
};
export const emailSlice = createSlice({
name: "email",
initialState,
reducers: {
setEmails: (state, action: any) => {
state.emails = action.payload;
},
},
});
export const { setEmails } = emailSlice.actions;
export const addNewEmail = (prevEmails: any, email: string): AppThunk => (
dispatch
) => {
const allEmails = prevEmails.push(email);
dispatch(setEmails(allEmails));
};
export default emailSlice.reducer;
export const selectEmails = (state: any) => state.emailReducer.emails;

I was also getting the same error, this is not how you disptach the
action. All you have to pass this middlewares in your store.
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
})
})

As #asaf-aviv said, the real problem is that you're attempting to mutate what is actually state.emails, outside of a reducer:
const allEmails = prevEmails.push(email);
dispatch(setEmails(allEmails));
The second problem is conceptual. You should model actions as "events", not "setters", and put as much logic as possible into reducers. If you follow those guidelines, this problem won't occur in the first place.
Also, this doesn't even need to be a thunk - just dispatch an action that contains the new email object.
The right way to handle this is:
export const emailSlice = createSlice({
name: "email",
initialState,
reducers: {
emailAdded: (state, action: PayloadAction<Email>) => {
state.emails.push(action.payload)
},
},
});
export const { emailAdded } = emailSlice.actions;
// later
dispatch(emailAdded(newEmail));

You are mutating the state before dispatching the action, you can do mutations inside the reducer but not outside of it.
You can change prevEmails.push(email) to prevEmails.concat(email) which will return a new array which you can then send as a payload.

Related

Why redux store doesn't receive an update from immer

Combining reducers
export default (injectedReducers = {}) => {
return combineReducers({
...injectedReducers,
memoizedStamps: memoizedStampsReducer, // <-- need to write here
});
};
Writing an action
const mapDispatch = (dispatch) => ({
addStamp: (payload) =>
dispatch({
type: ADD_STAMP,
payload,
}),
});
Writing the reducer
export const initialState = [];
const memoizedStampsReducer = produce((draft, action) => {
const { type, payload } = action;
switch (type) {
case ADD_STAMP:
draft.push(payload);
}
}, initialState);
export default memoizedStampsReducer;
Using in a react hook
const useMemoizedStamps = () => {
const [memStamps, dispatch] = useImmerReducer(reducer, initialState);
const { addStamp } = mapDispatch(dispatch);
useEffect(() => {
addStamp({ body: 'body', coords: 'coords' });
}, []);
console.log(memStamps); // <-- gives [{ body: 'body', coords: 'coords' }] all good here
return null;
};
export default useMemoizedStamps;
But it gets never saved into injected reducer "memoizedStamps". The array is always empty. It works perfectly will with connect(null, mapDispatchToProps), but can't use connect() in my custom hook.
What do I do wrong? What is the answer here?
--- UPD 1 ---
#phry, like so?
const useMemoizedStamps = (response = null, error = null) => {
const dispatch = useDispatch();
const [memStamps] = useImmerReducer(reducer, initialState);
const { addStamp } = mapDispatch(dispatch);
useEffect(() => {
addStamp({ body: 'body', coords: 'coords' });
}, []);
console.log(memStamps);
return null;
};
And now if I need them to be local, I need to use immer's dispatcher? Any way to merge these two dispatchers? P.S. this dispatcher really saved it to global state.
Rereading this, I think you just have a misconception.
Stuff is never "saved into a reducer". A reducer only manages how state changes.
In Redux, it would be "saved into the store", but for that you would have to actually use a store. useImmerReducer has nothing to do with Redux though - it is just a version of useReducer, which like useState just manages isolated component-local state with a reducer. This state will not be shared with other components.
If you want to use Redux (and use it with immer), please look into the official Redux Toolkit, which already comes with immer integrated. It is taught by the official Redux tutorial.

How to implement redux-toolkit and next,js and not lose SSR

I'm trying to implement redux-toolkit in my Next.js project without losing the option of SSR for the data I'm fetching from an external API. I have followed the example in next.js GitHub but doing so led to not having SSR when fetching data in my redux slice. I would like to know how I can fetch data and save that data in the Redux state. this is what I've written:
this is the store.js file
export const store = configureStore({
reducer: {
users: usersReducer,
},
});
the _app.js file
import { Provider } from 'react-redux';
import { store } from '../app/store';
const MyApp = ({ Component, pageProps }) => {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
};
export default MyApp;
the usersSlice.js file
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
const initialState = {
items: null,
status: 'idle',
error: null,
};
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await res.json();
return users;
});
const usersSlice = createSlice({
name: 'categories',
initialState,
reducers: {},
extraReducers: {
[fetchUsers.pending]: (state, action) => {
state.status = 'loading';
},
[fetchUsers.fulfilled]: (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
},
[fetchUsers.rejected]: (state, action) => {
state.status = 'failed';
state.error = action.error.message;
},
},
});
export default usersSlice.reducer;
and finally where the page I'm fetching the data from:
export default function Home() {
const dispatch = useDispatch();
const users = useSelector((state) => state.users.items);
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
return (
<div>
<h1>users</h1>
{users &&
users.length &&
users.map((user) => <p key={user.id}>{user.name}</p>)}
</div>
);
}
If I fetch data through dispatching the fetchUsers function it won't have SSR, and if I use getServerSideProps it won't be saved in redux state. I'm clueless
If you are okay to use Redux instead of redux-toolkit then follow this example
[https://github.com/vercel/next.js/blob/canary/examples/with-redux-persist]Officail Example form Vercel/next.js. I too facing some issue when i am writing SSR code for Redux persist with redux toolkit. The work is currently in progress. Will share the code when it is available. Sis

Calling other actions from createAsyncThunk

Usually in a thunk you'd wind up calling other actions:
const startRecipe = {type: "startRecipe"}
const reducer = (state, action) => {
if (action.type === "startRecipe") {
state.mode = AppMode.CookRecipe
}
}
const getRecipeFromUrl = () => async dispatch => {
const res = await Parser.getRecipeFromUrl(url)
dispatch(startRecipe)
}
With createAsyncThunk in redux toolkit, this isn't so straightforward. Indeed you can mutate the state from your resulting action in extraReducers:
export const getRecipeFromUrl = createAsyncThunk('getRecipeFromUrl',
async (url: string): Promise<RecipeJSON> => await Parser.getRecipeFromUrl(url)
)
const appStateSlice = createSlice({
name: 'app',
initialState: initialAppState,
reducers: {},
extraReducers: ({ addCase }) => {
addCase(getRecipeFromUrl.fulfilled, (state) => {
state.mode = AppMode.CookRecipe
})
}
})
But I also want to have non-async ways to start the recipe, which would entail a reducer in the slice:
reducers: {
startRecipe(state): state.mode = AppState.CookRecipe
},
To avoid writing the same code in two places I would love to be able to call the simple reducer function from the thunk handler. I tried simply startRecipe(state) and startRecipe (which had been destructured for ducks exporting so I’m fairly sure I was referring to the correct function) from the extraReducers case but it doesn't work.
My current solution is to define _startRecipe outside of the slice and just refer to that function in both cases
reducers: { startRecipe: _startRecipe },
extraReducers: builder => {
builder.addCase(getRecipeFromUrl.fulfilled, _startRecipe)
}
Is there a "better" way where you can define the simple action in your slice.reducers and refer to it from the thunk handler in extraReducers?
The second argument of the payloadCreator is thunkAPI (doc) from where you could dispatch the cookRecipe action.
interface ThunkApiConfig {
dispatch: AppDispatch,
state: IRootState,
}
export const getRecipeFromUrl = createAsyncThunk('getRecipeFromUrl',
async (url: string, thunkAPI: ThunkApiConfig): Promise<RecipeJSON> => {
await Parser.getRecipeFromUrl(url)
return thunkAPI.dispatch(cookRecipeActionCreator())
}
)
The idea of "calling a reducer" is the wrong approach, conceptually. Part of the design of Redux is that the only way to trigger a state update is by dispatching an action.
If you were writing the reducer using a switch statement, you could have multiple action types as cases that all are handled by the same block:
switch(action.type) {
case TypeA:
case TypeB: {
// common logic for A and B
}
case C: // logic for C
}
When using createSlice, you can mimic this pattern by defining a "case reducer" function outside of the call to createSlice, and pass it for each case you want to handle:
const caseReducerAB = (state) => {
// update logic here
}
const slice = createSlice({
name: "mySlice",
initialState,
reducers: {
typeA: caseReducerAB,
typeB: caseReducerAB,
}
extraReducers: builder => {
builder.addCase(someAction, caseReducerAB)
}
})
That sounds like what you described as your "current solution", so yes, that's what I would suggest.

Redux Dispatch Infinite Loop

I am using axios.get in my useeffect and then I am passing the data from response to the dispatcher. It retrieves data and dispatches and then I get the state to show data an console it but data shows infinite loop. Here is my useEffect, local state, mapStateToProps and mapDispatchToProps.
const [users, setUsers] = useState()
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users').then(response => {
// console.log(response.data)
__storeUsers(response.data)
})
setUsers(showUsers)
console.log(users)
}, [__storeUsers, showUsers, users, setUsers])
const mapStateToProps = state => ({
showUsers: state.getUsers.users
})
const mapDispatchToProps = dispatch => ({
__storeUsers: (data) => dispatch({type: types.STORE_USERS, payload: data}),
})
This is my reducer for users
import * as types from "./types";
const initialState = {
users: []
}
const usersState = (state = initialState, action) => {
switch (action.type) {
case types.STORE_USERS:
return {
...state,
users: action.payload
}
default:
return state
}
}
export default usersState
This is for practice purpose. i am not using actionCreators right now. After this I will move the axios call to the action creator. The data that I get from above goes in loop in console. Please help.
Also if I create action creator for this, that also goes in loop. Action creator is like below:
export const UserActions = () => async (dispatch) => {
const response = await axios.get('https://jsonplaceholder.typicode.com/users')
if (response.data) {
// console.log(response.data)
dispatch({
type: types.STORE_USERS,
payload: response.data
})
} else {
// console.log("no data")
}
return response
}
And then I use it like below
const mapDispatchToProps = dispatch => ({
__storeUsers: () => dispatch(UserActions())
})
Both methods are firing loop in console.
Notice that your useEffect here is where the infinite loop occurs:
const [users, setUsers] = useState()
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users').then(response => {
// console.log(response.data)
__storeUsers(response.data)
})
setUsers(showUsers)
console.log(users)
}, [__storeUsers, showUsers, users, setUsers])
The useEffect has been told that users is one of its dependencies and that it should re-run when this variable changes. The useEffect then changes the value of users via setUsers and it sees this update so runs again.
It looks like you're only depending on users for this console.log. Consider taking it out of the dependency list.

Redux request statuses. Reduce boilerplate

I am using Redux with react and redux-thunk as a middleware.
When I make http requests I have to dispatch three actions in my thunks.
I will use my auth example.
here are my actions:
export const loginSuccess = () => ({
type: AUTH_LOGIN_SUCCESS,
})
export const loginFailure = (errorMessage) => ({
type: AUTH_LOGIN_FAILURE,
errorMessage,
})
export const loginRequest = () => ({
type: AUTH_LOGIN_REQUEST,
})
and here is the thunk which combines above three actions:
export const login = (credentials) => dispatch => {
dispatch(loginRequest())
const options = {
method: 'post',
url: `${ENDPOINT_LOGIN}?username=${credentials.username}&password=${credentials.password}`,
}
axiosInstance(options)
.then(response => {
dispatch(loginSuccess())
dispatch(loadUser(response.data)) // I have separate action for user and separate reducer.
window.localStorage.setItem(ACCESS_TOKEN_KEY, response.data.token)
})
.catch(error => {
return dispatch(loginFailure(error))
})
}
And here is my reducer:
const initialState = {
pending: false,
error: false,
errorMessage: null,
}
export const loginReducer = (state = initialState, action) => {
switch (action.type) {
case AUTH_LOGIN_SUCCESS:
return {
...state,
pending: false,
error: false,
errorMessage: null,
}
case AUTH_LOGIN_FAILURE:
const { errorMessage } = action
return {
...state,
pending: false,
error: true,
errorMessage,
}
case AUTH_LOGIN_REQUEST:
return {
...state,
pending: true,
}
default:
return state
}
}
I have to do almost exact same things when I am sending another request, for example in case of logout. I feel like I am repeating myself a lot and there must be a better way.
I need to know what is the best practice to handle this issue.
Any other corrections and recommendations will be appreciated.
If you are looking for "ready to use" solution take a look at:
https://redux-toolkit.js.org/api/createAsyncThunk
https://redux-resource.js.org/ (but it is written with js (not TS), and no #types definition for this library)
If you are looking for a custom solution you can create a few factories:
factory for reducer
factory for three actions
factory for thunk
const actions = createActions('My request name');
const reducer = createReducer(actions);
...
const thunk = createThunk(config);
or even you can combine them:
const { actions, reducer, thunk } = createRequestState('Name...', config);
... but this is just an idea.

Resources