Any tips for mutating the state using redux-toolkit - reactjs

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

Related

Why is state.filter in Redux Toolkit not filtering the array?

I'm creating a goal tracker with Redux Toolkit for learning purposes. The goals are held in an array which I push new values to with no issues, but when I try to state.filter the same array, the value isn't removed. Redux devtools shows deleteGoal running, but it's not deleting anything.
I tried using splice, filtering current(state), logging state, and placing debuggers throughout the various files.
I noticed that state logs as a proxy, but if that were the problem, why does state.push() work and state.filter() doesn't?
The array (in the slice):
const initialState = [
{
id: 1,
input: 'drink water',
achieved: true
},
{
id: 2,
input: 'in bed by 11:15',
achieved: true
},
{
id: 3,
input: 'study 2hrs',
achieved: true
},
]
The actions:
export const goalsSlice = createSlice({
name: 'goals',
initialState,
reducers: {
addGoal: (state, action) => {
state.push(action.payload)
},
deleteGoal: (state, action) => {
const goalId = action.payload
state.filter((goal) => goal.id !== goalId)
console.log(current(state))
},
},
})
The component where it's rendered:
<Text
fontSize="3xl"
fontWeight={600}
>your goals:</Text>
{goals.map((goal, index) => (
<li
key={index}
onDoubleClick = {goal => {
dispatch(deleteGoal(goal.target.textContent))
// console.log(goal)
// console.log(goal.target.textContent)
}}
className='goal-text'
>
{goal.input}
</li>
))}
state.filter() is not working as you expect because you need to return the result in order to update the state; not change it in place. e.g.
return state.filter((goal) => goal.id !== goalId)
state.push() does work as you expect because it's changing the state in-place as opposed to returning a value. This is arguably against the best practice of Redux - your addGoal function could be changed to something like
addGoal: (state, action) => {
// return a new array, don't modify the existing state in place
return [
...state, // copy the current array of goals
(action.payload) // add the new goal
]
},
See https://redux.js.org/style-guide/#do-not-mutate-state for more details
Because filter() does not modify the array, rather it creates a shadow copy of it, perhaps the intended action in deleteGoal should be:
// Only for redux-toolkit
state = state.filter((goal) => goal.id !== goalId)
According to redux-toolkit document (as opposed to Redux):
To make things easier, createReducer uses immer to let you write reducers as if they were mutating the state directly. In reality, the reducer receives a proxy state that translates all mutations into equivalent copy operations.
More examples about how redux-toolkit allows "mutating" update of immutable state in reducers for: createSlice.
Redux tookit use immer under the hood that allow you to write mutable syntax. I guess its the must thing to not write it immutable code else there is no use of immer. I face same problem and simple return the new array from filter.`
erase: (state, action) => {
return state.filter(
(element) => JSON.stringify(element) !== JSON.stringify(action.payload)
);
`
it removed the object that didnt match. and it worked.

Why can't I update the state using Redux?

I'm trying to integrate Redux in a project that works already. I've configured the Redux store using multiple slices, here's the one causing troubles:
const initialCategoriesState = [];
const categoriesSlice = createSlice({
name: "categories",
initialState: initialCategoriesState,
reducers: {
setCategories(state, action) {
state = action.payload;
},
},
});
In my component I'm using useSelector to access the state:
const categories = useSelector(state => state.categories);
And to update it I dispatch an action, accordingly with the one declared in the slice:
const fetchedCategories = await fetchData(urlCategories, CATEGORIES);
dispatch(categoriesActions.setCategories(fetchedCategories));
But once I run the code, the categories constant gets never updated. Since I wasn't sure the action was getting the data, I tried to console.log the state inside the reducer it as it follows:
reducers: {
setCategories(state, action) {
console.log("state before", state);
state = action.payload;
console.log("state after", state);
},
},
The state is indeed changing, but not the extracted state in the categories constant.
Is there something I'm missing here?
I've got the same problem a few months ago and solved it this way. But, I may be missing something too.
Please try this for your initial state:
const initialCategoriesState = { categories: [] };
And then in your reducer:
state.categories = action.payload;
I've found one of the cool things about Redux Toolkit to be it's Entity Adapters, which can safely initialize your state while providing helper functions for updating your state. They (reasonably) assume that your slice will have collections, and probably a main collection of things with the same name as the slice. createEntityAdapter() allows you to do like:
const categoriesAdapter = createEntityAdapter()
// name the below "initialState" exactly
const initialState = categoriesAdapter.getInitialState({
// this will, by default, get you an `entities{}` and `ids[]` representing your categories
// you can add any additional properties you want in state here as well
})
Then, when you are wanting to update state, in your reducer you can do like:
reducers: {
setCategories(state, action) {
categoriesReducer.setAll(state, action.payload)
// this will update both entities{} and ids[] appropriately
},
},

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 can you replace entire state in Redux Toolkit reducer?

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

Resources