Add an object to an array using redux toolkit - reactjs

everyone. I want to store an Object into an array using redux toolkit. Unfortunately, it is the case that a new object is not added, but simply replaced. How can I change this behavior ?
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
warenkorb: [],
preis: 0,
bewertung: 0,
};
export const produktSlice = createSlice({
name: "produkt",
initialState,
reducers: {
hinzufügen: (state, action) => {
// Also not work
// state.warenkorb.push(action.payload);
//
state.warenkorb.push([...state.warenkorb, action.payload]);
state.preis += action.payload.preis * action.payload.anzahl;
},
entfernen: (state, action) => {
if (state.warenkorb.includes(action.payload)) {
state.warenkorb.splice(
state.warenkorb.findIndex((id) => id === action.payload),
1
);
}
},
bewerten: (state, action) => {
state.bewertung = action.payload.bewertung;
},
},
});
export const { hinzufügen, entfernen, bewerten } = produktSlice.actions;
export default produktSlice.reducer;
As you can see, a new call is started each time with the new object
And if I look at it with the help of the debugger, I get to see this. Why the first two objects and why do they look like this?

You should not push into the state you should replace the current state with a whole new state to ensure state updates
The code should be:
hinzufügen: (state, action) => {
state.warenkorb = [...state.warenkorb, action.payload];
}
I made this sandbox for the total working example

Related

Update a react-redux state too fast does not take the modification into account

I have an object stored in react-redux which contains multiple sub-objects, like:
MyObject =
{
{ id: 1, name: "object 1"}
{ id: 2, name: "object 2"}
...
}
This object can be updated very quickly multiple times, for example with a function like this:
function modifyMyObject() {
//Load the object from Redux and create a clone to be modified
let myObject = JSON.parse(JSON.stringify(this.props.myObject))
//Change the properties of my object
...
//Update the object on Redux
this.props.setMyObject(myObject)
}
However I noticed that if I call modifyMyObject() very quickly with different modifications, the object is not updated properly.
I guess that the state in redux does not have time to be updated before I try to make a new modification.
Here is the object slice :
import {createSlice} from '#reduxjs/toolkit'
const initialState = {
value: {},
}
export const myObjectSlice = createSlice({
name: 'object',
initialState,
reducers: {
setMyObject: (state, action) => {
state.value = action.payload
},
},
})
// Action creators are generated for each case reducer function
export const { setMyObject } = myObjectSlice.actions
export default myObjectSlice.reducer
Is there a better way to handle these quick changes? Or do you have a suggestion to improve this code? Thank you!
I'm posting here the solution to this problem which was suggested by HåkenLid:
Method which is NOT working: in App.js, create a copy of the slice.state, modify it and save it to Redux:
function modifyMyObject() {
//Load the object from Redux and create a clone to be modified
let myObject = JSON.parse(JSON.stringify(this.props.myObject))
//Change the properties of my object
...
//Update the object on Redux
this.props.setMyObject(myObject)
}
The problem: the object is out of date when saved.
The solution: In app.js:
function modifyMyObject(newObject) {
this.props.updateMyObject(newObject) //call the function from the slice
}
In MyObjectSlice.js:
import {createSlice} from '#reduxjs/toolkit'
const initialState = {
value: {},
}
export const myObjectSlice = createSlice({
name: 'object',
initialState,
reducers: {
updateMyObject: (state, action) => {
let newObject = action.payload
//-> insert here the logic to update the object <-
...
state.value = newObject
},
},
})
// Action creators are generated for each case reducer function
export const { updateMyObject } = myObjectSlice.actions
export default myObjectSlice.reducer
Are you trying try/catch block to handle JSON.parse? I hope this below can help you:
function modifyMyObject() {
function parseJSONSafely(str) {
try {
return JSON.parse(str);
}
catch (e) {
console.err(e);
// Return a default object, or null based on use case.
return {}
}
}
//Load the object from Redux and create a clone to be modified
editedObject = parseJSONSafely(JSON.stringify(this.props.myObject))
if (editedObject !== {}) {
//Change the properties of my object
editedObject['proterty'] = /* ... */
//Update the object on Redux
this.props.setMyObject(editedObject)
}
}

How do you write redux reducers for wide data types?

I'm currently working on a project that implements something similar to MS Project - A task management app with very wide task properties.
A super simple definition of a task might be like:
{
"id": 0,
"description": "Task 1",
"work": 8,
"startDate": 0,
"assignedResource": "Scott"
}
However the expectation is that a task may have 50+ fields eventually.
I'm fairly new to React and React-Redux so forgive me if I'm misunderstanding some simple approach, but I cannot understand how it is expected to write reducer actions for a data type like this.
Here is an example reducer for the above task:
import { createSlice, current } from '#reduxjs/toolkit'
export const tasksSlice = createSlice({
name: 'tasks',
initialState: [],
reducers: {
addTask: (state, action) => {
state.push(action.payload);
},
deleteTask: (state, action) => {
return state.filter(task => task.id !== action.payload);
},
resetTasks: (state, action) => {
let tasks = action.payload;
return tasks;
},
replaceTask: (state, action) => {
const taskIndex = state.tasks.findIndex(task => task.id == action.payload.id)
return {
...state,
tasks: [
...state.tasks.slice(0, taskIndex),
action.payload,
...state.tasks.slice(taskIndex + 1)
]
}
},
setTaskDescription: (state, action) => {
state.map(t => t.id !== action.payload.id ? t : { ...t, description: action.payload.description })
}
}
})
export const { resetTasks, addTask, deleteTask, replaceTask } = tasksSlice.actions
export default tasksSlice.reducer
I wrote replaceTask as a quickhand attempt to just write the needed changes dispatched wholesale. But this feels wrong - I cannot guarantee validation or some mistaken mutation doesn't find its way into the store. On the other hand I cannot fathom having to write an action for every single field of the task.
In constrast it seems most examples want you to write actions like setTaskDescription, but that would require me to write a ton of boilerplate.
Is there a standard practice for this I'm missing?
Assuming you have tasks like:
state = {
tasks: []
}
First find the index:
const taskIndex = state.tasks.findIndex(task => task.id == action.payload.id)
Now, replace it:
return {
...state,
tasks: [
...state.tasks.slice(0, taskIndex),
action.payload,
...state.tasks.slice(taskIndex+1)
]
}

How do I update a nested Item in a different reducer (slice) with redux toolkit in a React app

I have the following code (I deleted most of it to make it easier to understand - but everything works):
"role" reducer:
// some async thunks
const rolesSlice = createSlice({
name: "role",
initialState,
reducers: { // some reducers here },
extraReducers: {
// a bunch of extraReducers
[deleteRole.fulfilled]: (state, { payload }) => {
state.roles = state.roles.filter((role) => role._id !== payload.id);
state.loading = false;
state.hasErrors = false;
},
},
});
export const rolesSelector = (state) => state.roles;
export default rolesSlice.reducer;
"scene" reducer:
// some async thunks
const scenesSlice = createSlice({
name: "scene",
initialState,
reducers: {},
extraReducers: {
[fetchScenes.fulfilled]: (state, { payload }) => {
state.scenes = payload.map((scene) => scene);
state.loading = false;
state.scenesFetched = true;
}
}
export const scenesSelector = (state) => state.scenes;
export default scenesSlice.reducer;
a component with a button and a handleDelete function:
// a react functional component
function handleDelete(role) {
// some confirmation code
dispatch(deleteRole(role._id));
}
My scene model (and store state) looks like this:
[
{
number: 1,
roles: [role1, role2]
},
{
number: 2,
roles: [role3, role5]
}
]
What I am trying to achieve is, when a role gets deleted, the state.scenes gets updated (map over the scenes and filter out every occurrence of the deleted role).
So my question is, how can I update the state without calling two different actions from my component (which is the recommended way for that?)
Thanks in advance!
You can use the extraReducers property of createSlice to respond to actions which are defined in another slice, or which are defined outside of the slice as they are here.
You want to iterate over every scene and remove the deleted role from the roles array. If you simply replace every single array with its filtered version that's the easiest to write. But it will cause unnecessary re-renders because some of the arrays that you are replacing are unchanged. Instead we can use .findIndex() and .splice(), similar to this example.
extraReducers: {
[fetchScenes.fulfilled]: (state, { payload }) => { ... }
[deleteRole.fulfilled]: (state, { payload }) => {
state.scenes.forEach( scene => {
// find the index of the deleted role id in an array of ids
const i = scene.roles.findIndex( id => id === payload.id );
// if the array contains the deleted role
if ( i !== -1 ) {
// remove one element starting from that position
scene.roles.splice( i, 1 )
}
})
}
}

Redux Toolkit: 'Cannot perform 'set' on a proxy that has been revoked'

I'm trying to recreate a Memory-like game with React. I'm using Redux Toolkit for state management, but I'm having trouble with one use case.
In the selectCard action, I want to add the selected card to the store, and check if there's already 2 of them selected. If so, I want to empty the selected array after a delay.
const initialState : MemoryState = {
cards: [],
selected: [],
}
const memorySlice = createSlice({
name: 'memory',
initialState: initialState,
reducers: {
selectCard(state: MemoryState, action: PayloadAction<number>) {
state.selected.push(action.payload);
if (state.selected.length === 2) {
setTimeout(() => {
state.selected = [];
}, 1000);
}
}
}
});
The cards get selected just fine, but when I select 2 I get this error after 1 sec:
TypeError: Cannot perform 'set' on a proxy that has been revoked, on the line state.selected = [];
I'm new to this stuff, how do I access the state after a delay? Do I have to do it asynchronously? If so, how?
As stated in their documentation, don't perform side effects inside a reducer.
I would add the setTimeout when dispatching the action instead:
// so the reducer:
...
if (state.selected.length === 2) {
state.selected = [];
}
...
// and when dispatching
setTimeout(() => {
dispatch(selectCard(1))
}, 1000)
I ran into this issue too. I solved it by using the 'side effect' code into a function and then used the result of it in the reducer
const initialState : MemoryState = {
cards: [],
selected: [],
}
const memorySlice = createSlice({
name: 'memory',
initialState: initialState,
reducers: {
selectCard(state: MemoryState, action: PayloadAction<number>) {
state.selected = action.payload
}
}
});
export const { selectCard } = memorySlice.actions
export const sideEffectFunc = (param) => (dispatch) => {
let selected = []
selected.push(action.payload);
if (selected.length === 2) {
setTimeout(() => {
selected = [];
}, 1000);
}
dispatch(selectCard(selected));
};
Don't pay attention to the logic of the function (haven't tested it and it might be wrong) but I wanted to show the way we could handle 'side effect' code when using redux toolkit

redux-toolkit sharing state between reducer

I building small budget calculator and its the first time i am using redux-toolkit, the problem is
How can share/pass state between reducers in redux-toolkit ? (how can use the totalIncomes and totalExpenses in the balance slice to calculate the total balance ?
another question is is ok to use redux-toolkit instead of plain redux
incomes.js :
const incomesSlice = createSlice({
name: "incomes",
initialState: {
list: [],
loading: false,
totalIncomes: 0,
lastFetch: null,
},
reducers: {
ADD_INCOME: (state, action) => {
state.list.push({
id: uuidv4(),
description: action.payload.description,
amount: action.payload.amount,
});
},
REMOVE_INCOME: (state, action) => {
const index = state.list.findIndex(
(income) => income.id === action.payload.id
);
state.list.splice(index, 1);
},
TOTAL_INCOMES: (state, action) => {
state.totalIncomes = state.list.reduce(
(acc, curr) => acc + curr.amount,
0
);
},
},
});
expenses.js :
const expensesSlice = createSlice({
name: "expenses",
initialState: {
list: [],
loading: false,
totalExpenses: 0,
lastFetch: null,
},
reducers: {
ADD_EXPENSE: (state, action) => {
state.list.push({
id: uuidv4(),
description: action.payload.description,
amount: action.payload.amount,
});
},
REMOVE_EXPENSE: (state, action) => {
const index = state.list.findIndex(
(expense) => expense.id === action.payload.id
);
state.list.splice(index, 1);
},
TOTAL_EXPENSES: (state, action) => {
state.totalExpenses = state.list.reduce(
(acc, curr) => acc + curr.amount,
0
);
},
},
});
export const {
ADD_EXPENSE,
REMOVE_EXPENSE,
TOTAL_EXPENSES,
} = expensesSlice.actions;
export default expensesSlice.reducer;
balance.js :
const balanceSlice = createSlice({
name: "balance",
initialState: {
total: 0
},
reducers: {
CALC_TOTAL: (state, action) => {
// How to Calculate this ?
},
},
});enter code here
export const { CALC_TOTAL } = balanceSlice.actions;
export default balanceSlice.reducer;
For anyone looking into this - author's is the wrong approach to using redux for state management.
When using redux you want your state as normalized as possible - you shouldn't store uneeded/duplicated state or state that can be calculated based on other state, in this example there's no need to save totalIncomes since we can calculate this based on the list of incomes (same goes for totalExpenses and balance).
As mentioned, the totalIncomes shouldn't be part of the state but should be a calculated value, you can either calculate it on the fly or use a selector. In the below example I'll use a selector.
Redux Toolkit solution
To use it with Redux toolkit it might look something like this, I've removed parts of code for brewity:
incomes slice
// ...
const incomesSlice = createSlice({
name: "incomes",
initialState: {
list: [],
},
reducers: {
ADD_INCOME: (state, action) => {
state.list.push({
id: uuidv4(),
description: action.payload.description,
amount: action.payload.amount,
});
},
REMOVE_INCOME: (state, action) => {
const index = state.list.findIndex(
(income) => income.id === action.payload.id
);
state.list.splice(index, 1);
},
},
});
export const getTotalIncome = createSelector(
totalIncomeSelector,
calculateTotalIncome,
);
export function totalIncomeSelector(state) {
return state.incomes.list;
}
export function calculateTotalIncome(incomesList) {
return incomesList.reduce((total, income) => total + income.amount);
}
export const {
ADD_INVOICE,
REMOVE_INVOICE,
} = incomesSlice.actions;
export default incomesSlice.reducer;
expenses slice - removed parts for brewity
// ...
const expensesSlice = createSlice({
name: "expenses",
initialState: {
list: [],
},
reducers: {
ADD_EXPENSE: (state, action) => {
state.list.push({
id: uuidv4(),
description: action.payload.description,
amount: action.payload.amount,
});
},
REMOVE_EXPENSE: (state, action) => {
const index = state.list.findIndex(
(income) => income.id === action.payload.id
);
state.list.splice(index, 1);
},
},
});
export const getTotalExpense = createSelector(
totalExpenseSelector,
calculateTotalExpense,
);
export function totalExpenseSelector(state) {
return state.expenses.list;
}
export function calculateTotalExpenseexpenseList) {
return expensesList.reduce((total, expense) => total + expense.amount);
}
export const {
ADD_EXPENSE,
REMOVE_EXPENSE,
} = expensesSlice.actions;
export default expensesSlice.reducer;
balance slice - you don't really need a slice here, you just need a selector
import { getTotalIncome, totalIncomeSelector } from './incomeSlice';
import { getTotalExpense, totalExpenseSelector } from './expenseSlice';
export const getBalance = createSelector(
getTotalIncome,
getTotalExpense,
(totalIncome, totalExpense) => totalIncome - totalIncome,
);
Example component
// ...
function BalanceComponent({
totalIncome,
totalExpense,
balance,
}) {
return (
<div>
<h1>Finance overview</h1>
<div>
<span>Total Income:</span>
<span>{totalIncome}</span>
</div>
<div>
<span>Total Expense:</span>
<span>{totalExpense}</span>
</div>
<div>
<span>Balance:</span>
<span>{balance}</span>
</div>
</div>
);
}
function mapStateToProps(state) {
return {
totalIncome: getTotalIncome(state),
totalExpense: getTotalExpense(state),
balance: getBalance(state),
}
}
export default connect(mapStateToProps)(BalanceComponent);
Note: In the question the author seems to be breaking up his state into too many slices, all this can be a lot simpler by having it all as a single slice. That's what I would do.
Is it ok to use redux-toolkit instead of plain redux
YES. It was originally created to help address common concerns about Redux. See its purpose.
How can share/pass state between reducers in redux-toolkit?
You can pass the used state parts to action.payload.
dispatch(CALC_TOTAL(totalIncomes,totalExpenses))
You can use extraReducers and "listen" to to your incomes/expenses changes.
You can create a middleware or use createAsyncThunk where you can reference the most updated state with getState().
Toolkit docs.

Resources