How can you replace entire state in Redux Toolkit reducer? - reactjs

EDIT: The solution is to return state after I replace it completely (return state = {...action.payload})! But why? I don't need to return it when I replace the fields individually.
I'm working with the Redux Toolkit, which simplifies some Redux boilerplate. One thing they do is use Immer to allow you to directly 'modify' state (in fact, you're not). It works fine, except I don't know how to replace my section of state entirely. For instance, I want to do something like this
const reducer = createReducer({ s: '', blip: [] }, {
[postsBogus.type]: (state, action) => {
state = { ...action.payload };
}
but state remains untouched. Instead I have to do this
[postsBogus.type]: (state, action) => {
state.s = action.payload.s;
state.blip = action.payload.blip;
}
Is there a way I can replace state entirely?

Yes, as you noted, you must return a new value to replace the state entirely.
Even in a "plain" Redux reducer, assigning state = newValue does nothing, because all that does is say that the local function variable named state is now pointing to a different value in memory. That does nothing to return a new value.
For Immer specifically, you can either mutate the contents of the Proxy-wrapped state value as long as it's an object or array, or you can return an entirely new value, but not both at once.

You can, but not in this way, when you say:
function x(y){
y = 4
}
You're mutating the function parameter, but not the state of redux,
you have two options to update this state of your redux store:
either to set state.your_state_name = something or, in your case, what you want to do is to return a new object, the new object is what the new state value is going to be.
simple example:
myReducerFunc: (state, action) => {
return {...action.payload }
},
another example:
const loggedInUserSlice = createSlice({
name: '$loggedInUser',
initialState: {
isLoggedIn: false,
},
reducers: {
loggedIn: (state, action) => {
return {
isLoggedIn: true,
...action.payload,
}
},
},
})

Related

Any tips for mutating the state using redux-toolkit

I'm using redux-toolkit and React.
I know basic redux (not using redux-toolkit) aren't allowed to mutate the original state. That's why I chose redux-toolkit to be able to do it. However,I can't understand what's a forbidden way to mutate the state using redux-toolkit. I read an offical document below but it didn't work to me.
https://redux-toolkit.js.org/usage/immer-reducers
Especially Immer Usage Patterns in the document.What's the difference between Mutating and Returning state and Resetting and Replacing state? It looks like similar. Plus, I can't figure out Updating Nested Data in the document.
could you tell me any tips for using redux-toolkit?
The intro of the page you've linked to effectively covers a bit of history for how Redux reducer functions worked in that past. Redux state is considered to be immutable, so if you wanted to update any part of a slice's state you necessarily needed to shallow copy all state and nested state that you wanted to update.
See Reducers and Immutable State:
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue,
},
},
},
}
}
Redux-Toolkit comes with immer baked right in, so we can write mutable reducer functions and know we are only editing a draft copy of the state that will get applied in an immutable way.
You are interested in the Immer Usage Patterns, specifically Mutating and Returning State.
In any given case reducer, Immer expects that you will either mutate
the existing state, or construct a new state value yourself and return
it, but not both in the same function! For example, both of these are
valid reducers with Immer:
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded(state, action) {
// "Mutate" the existing state, no return value needed
state.push(action.payload)
},
todoDeleted(state, action.payload) {
// Construct a new result array immutably and return it
return state.filter(todo => todo.id !== action.payload)
}
}
})
You can either mutate a part of the state and the change will be correctly applied or you can return an entirely new state object with as much or as little of it updated as you need. The only thing that is forbidden is to mutate part of the existing state and return a new state object reference.
Forbidden update:
const todosSlice = createSlice({
name: 'todos',
initialState: {todos: [], status: 'idle'}
reducers: {
todoDeleted(state, action.payload) {
// Construct a new array immutably
const newTodos = state.todos.filter(todo => todo.id !== action.payload);
// "Mutate" the existing state to save the new array
state.todos = newTodos;
return {
...state,
status: 'complete',
};
}
}
});
Acceptable update:
const todosSlice = createSlice({
name: 'todos',
initialState: {todos: [], status: 'idle'}
reducers: {
todoDeleted(state, action.payload) {
// Construct a new array immutably
const newTodos = state.todos.filter(todo => todo.id !== action.payload);
// "Mutate" the existing state to save the new array
state.todos = newTodos;
state.status = 'complete';
}
}
});

Is mutating "copy" of initial state inside a passed reducer function in "Redux Toolkits" createSlice() a bad practice?

Context
I'm trying to create the state for a multi-level accordion menu, where the top-level items are called subjects, each subject has multiple chapters and each chapter will have multiple articles.
At any given time only a single "Subject" can be in the "selected" state. The same applies to chapters and articles, with the additional restriction being they need to be "Children" of a parent in the "selected" state.
Code
I have a deeply nested object that is to be passed as the initial state to the createSlice() method,it has the following shape,
const initialState = {
currentArticle: null,
currentChapter: null,
currentSubject: null
subjects: [
{
id:"001",
chapters: [
{
id: "001001",
articles: [
{
id: "001001001",
selected: false
},
//....... more articles
],
selected: false
},
//....... more chapters
],
selected: false
},
//....... more subjects
]
}
Following is my createSlice() method,
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = initialState.subjects.map((subject) => {
if (payload.id === subject.id) {
subject.selected = true;
state.currentSubject = subject.id
}
return subject;
});
state.subjects = newNavigationState;
},
// ...... more reducer functions
},
});
The subjects array is directly used for rendering the UI, Every time a dispatch function is called I conditionally use the initial state and then calculate the next state,(those conditions are not included in the following code snippets for simplicities sake), For now let's consider that I use the initial states "subject" array every single time I need to calculate the next state instead of using the previous state passed to the reducer.
The reason for using the initial state is to not have to manually set the selected state of nested objects to false, in case the parents selected state changes.
Problem
However when I dispatch an action that executes the "setTopic" reducer function I get the following error,
TypeError: Cannot assign to read only property 'selected' of object '#<Object>'
Attempts to solve the issue
Using the spread operator to create a new copy of initialState within the reducer.
const copyInitialState = { ...initialState }
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
Using Object.assign() to create a new Object, within the reducer
const copyInitialState = {};
Object.assign(copyInitialState, initialState);
const newNavigationState = copyInitialState.subjects.map((subject) => {
//............
create 2 copies of the initial state, before invoking createSlice and pass one copy inside the createSlice() invocation as the initial state and use the other copy within the passed reducer function.
const initialStateCopy = Object.assign(initialState);
const initializedInitialState = Object.assign(initialState);
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initializedInitialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = initialStateCopy.subjects.map((subject) => {
//............
I.E : I tried this approach with the spread operator as well.
The only solution that works(not a desirable approach )
explicitly declare a completely new constant and initialize it in the exact same way as the initialState object. In this case, this simply means I'm copying the exact same object creation code one after the other so that they are completely two different objects,
const initialState = {//.... deeply nested object}
const initialStateExplicitCopy = {//.... deeply nested object}
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = initialStateExplicitCopy.subjects.map((subject) => {
Question
I think this has to do something with Immer and how it treats the initial state Object. I see that even though I do an Object.assign() nested objects are sealed and frozen.
Does this mean I'm attempting to perform something wrong? or something which is considered bad practice? Does this in any way make the reducer impure? If so I don't see why because the initial state never changes, I'm just using the initial state all the time to calculate the next state.
Is there a better approach to handle this while using the redux toolkit?
Yeah, the problem is the attempt to mutate initialState, for a couple different reasons.
It's only safe to write code that "mutates" data if that data has actually been passed through Immer and wrapped in proxies, so that Immer can track the attempted changes. When you reference initialState, that object hasn't been handed to Immer yet, so your code really is trying to mutate initialState.
Fortunately, when you call createSlice({initialState: someInitialStateValue}), createSlice internally "freezes" that value to make sure you don't accidentally mutate it for real later on. That's why you're getting the error - it's telling you you are doing something wrong.
Conceptually, I'm not sure why you're trying to always base the calculations off of initialState. Wouldn't you want to be doing updates based on the current state as the starting point?
If you truly do need to use initialState as the starting point, the best option is to use Immer directly and feed it initialState. Immer's main function is exported from RTK as createNextState, so you can use that to wrap your current logic
import { createNextState } from "#reduxjs/toolkit";
export const articleNavigationSlice = createSlice({
name: "articlenav",
initialState: initialState,
reducers: {
setTopic: (state, { payload }) => {
const newNavigationState = createNextState(initialState.subjects, draftSubjects) => {
const subject = draftSubjects.find(subject => subject.id === payload.id);
if (subject) {
subject.selected = true;
state.currentSubject = subject.id
}
}
state.subjects = newNavigationState;
},
}
});

Redux Toolkit - I can't update Slice state?

I wanna update the state and tried few ways to do that, but I can't.
First, I got a problem with fetching state, as a result, I got proxy, not a state.
That is fixed by the current() function by the redux toolkit.
Next, where I have a problem is mutation main slice state.
Here is a reducer for mutating.
reducers: {
setUser: (state, payload) => {
console.log("before", current(state)); //init state
state.currentUser = {
...state.currentUser,
loggined: true,
};
console.log("after", current(state)); // here I have new state but...
},
clearUser: (state) => {},
},
As a result, as a second console log, I got to state what I want, when I want to change a page or just want to do something with new data in the state, by useSelector() redux function, as a result, I got old, not changed state.
Why has this happened?
Example of Slice state:
initialState: {
currentUser: {
loggined: false,
isAdmin: false,
jwt: false,
},
},
Thanks!
Reducers of createSlice use immer:
This object will be passed to createReducer, so the reducers may safely "mutate" the state they are given.
So you can either return a new object that is the new state or "mutate" it and not return it, from createReducer
you need to ensure that you either mutate the state argument or return a new state, but not both.
So you can do:
setUser: (state, payload) => {
//state here is an immer draft, do not use that to copy current state
console.log("before", current(state)); //init state
state.currentUser.loggined = true;
//not returning anyting
},
Not sure how you'd return a new state based on the old one since ...state makes a copy of the immer draft and not of the state. Can't even find examples of doing this unless it's an array.

How to reset a redux slice state with Redux Toolkit (RTK)

I am trying to reset state via a redux action called resetState, where I assign the state to the initialState variable, however this does not work.
const initialState = {
someArray: [],
someEvents: {}
}
const stateSlice = createSlice({
name: "someState",
initialState,
reducers: {
...someActions,
resetState: (state, action) => {
// THIS DOES NOT WORK
state = initialState
},
resetState2: (state, action) => {
// THIS WORKS!!!!
return initialState
},
});
When I return the initialState, the state resets properly, but assigning it to initialState does not.
Assigning state = anything is not correct, regardless of whether you're using Redux Toolkit or writing the reducers by hand. All that does is point the local state variable to a different reference.
RTK's createSlice uses Immer inside. Immer primarily works by tracking mutations to a wrapped value, such as state.someField = 123. Immer also allows you to return an entirely new state you've constructed yourself.
So, in this case, what you want is return initialState to return a new value and force Immer to replace the old one.
resetState: (state, action) => {
Object.assign(state, action.payload)
},

How to update multiple state properties with immer.js

I wonder if it is possible to update multiple properties of state with immer.js in one "call".
Say I have state:
export const initialState = {
isUserLogged: false,
menuIsClosed: false,
mobileMenuIsClosed: true,
dataArray: ["dog","cat"],
};
And action creator:
export function updateSearchPage(data) {
return {
type: UPDATE_SEARCH_PAGE,
payload: data
};
}
I then use that action creator in React component like this:
this.props.updateSearchPage({
isUserLogged: true,
menuIsClosed: true,
dataArray: ["dog","cat","owl"]
})
The idea is that I want to update several properties of state at the same time. But I dont know which properties it is in advance. I know how to do it with a simple reducer:
case UPDATE_SEARCH_PAGE:
return Object.assign({}, state, action.payload)
But how to update several properties of state with immer at the same time? When the state properties (which one should update) are unknown in advance.
You can cycle on action.payload like the following:
const yourReducer = (state, action) =>
produce(state, draft => {
switch (action.type) {
case UPDATE_SEARCH_PAGE:
Object.entries(action.payload).forEach(([k, v]) => {
draft[k] = v;
})
break;
// ... other
}
}
Also: remember that on recent versions of immer is perfectly legit to returns an object, so doing return Object.assign({}, state, action.payload) is still valid inside a produce call.
With ES6 you can do it this way:
export const state = produce((draft, action) => {
switch (type) {
case UPDATE_SEARCH_PAGE:
return {...draft, ...action.payload}
}
}, initialState)
In this case it works the same way as without Immer. All properties will be merged (shallow merge) into state. If you need to replace the state just return action.payload
Immer gives you a draft state that you can edit. Behind the scenes it uses ES6 proxies to discover what you changed and apply in an immutable way your edits to the original state.
Basically, you can do the exact same thing you do right now, but using the Immer api:
import produce from 'immer'
const newState = produce(this.state, draft => Object.assign({}, draft, payload))
If you, instead, know what properties are changed, you can do something like:
const newState = produce(this.state, draft => {
draft.propertyOne = 'newValue'
draft.propertyTwo = 42
})

Resources