redux-toolkit sharing state between reducer - reactjs

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.

Related

Error: [Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft

So, I am making this shopping cart in redux-toolkit, where user can't order more than the quantity available. So, my reducer function is like below for adding to cart.
import { createSlice } from "#reduxjs/toolkit";
import { FoodCartType } from "../../types";
type CartState = {
cart: FoodCartType[];
};
const initialState: CartState = {
cart: [],
};
export const foodcartSlice = createSlice({
name: "foodcart",
initialState,
reducers: {
addToCart: (state, action) => {
console.log(action);
if (state.cart.length === 0) {
state.cart.push(action.payload);
}
const itemIndex = state.cart.findIndex(
(item) => item.id === action.payload.id
);
if (itemIndex >= 0) {
return {
...state,
cart: state.cart.map((item) => {
if (item.id === action.payload.id) {
return {
...item,
quantity: item.quantity + 1,
quantity_available: item.quantity_available - 1,
};
}
}),
};
} else {
return {
...state,
cart: [
...state.cart,
{
...action.payload.product,
quantity: 1,
quantity_available: action.payload.quantity_available - 1,
},
],
};
}
},
},
});
export const { addToCart } = foodcartSlice.actions;
export default foodcartSlice.reducer;
When I click on the dispatch, on the component. I get the following error.
Unhandled Runtime Error Error: [Immer] An immer producer returned a
new value and modified its draft. Either return a new value or
modify the draft.
So, where I am doing the problem? if I just remove the itemIndex check part, items get pushed into the cart. But, other than that it is giving me this error. How should I rewrite this?
Because your state.cart.push(action.payload); line modifies the value of state and later you return a new value.
You are allowed to do one, but not both at once. See writing reducers with immer
What you could do here: always modify. That's also far more readable.
export const foodcartSlice = createSlice({
name: "foodcart",
initialState,
reducers: {
addToCart: (state, action) => {
console.log(action);
// you push later anyways, so you can also delete these lines
// if (state.cart.length === 0) {
// state.cart.push(action.payload);
// }
const itemIndex = state.cart.findIndex(
(item) => item.id === action.payload.id
);
if (itemIndex >= 0) {
state.cart[itemIndex].quantity += 1
state.cart[itemIndex].quantity_available -= 1
} else {
state.cart.push({
...action.payload.product,
quantity: 1,
quantity_available: action.payload.quantity_available - 1,
})
}
},
},
});

Add an object to an array using redux toolkit

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

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

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

Filter products depend on another ACTION in React-native Redux

I have an app which get all categories and products from the server with Redux ACTIONS. I need to filter products with a category Id. after load data action is complete, i call another action to filter products but i'm a little bit confused.
There is codes of few parts of the app:
ProductsActions:
export const GET_INITIAL_PRODUCTS_DATA = "GET_INITIAL_PRODUCTS_DATA";
export const GET_INITIAL_PRODUCTS_DATA_RESULT = "GET_INITIAL_PRODUCTS_DATA_RESULT";
export const GET_INITIAL_PRODUCTS_DATA_ERROR = "GET_INITIAL_PRODUCTS_DATA_ERROR";
export const FILTER_PRODUCTS_BY_CATEGORY_ID = "FILTER_PRODUCTS_BY_CATEGORY_ID";
export const getInitialProductsData = () => ({
type: GET_INITIAL_PRODUCTS_DATA
});
export const filterProductsByCategoryId = categoryId => ({
type: FILTER_PRODUCTS_BY_CATEGORY_ID,
categoryId
});
ProductsReducers:
import {
GET_INITIAL_PRODUCTS_DATA,
GET_INITIAL_PRODUCTS_DATA_RESULT,
GET_INITIAL_PRODUCTS_DATA_ERROR,
FILTER_PRODUCTS_BY_CATEGORY_ID
} from "../actions/products";
const initialState = {
isFetching: false,
data: {},
error: null
};
const filterProductsByCategoryId = (state, action) => {
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case GET_INITIAL_PRODUCTS_DATA:
return {
...state,
isFetching: true
};
case GET_INITIAL_PRODUCTS_DATA_RESULT:
return {
...state,
isFetching: false,
data: action.result
};
case GET_INITIAL_PRODUCTS_DATA_ERROR:
return {
...state,
isFetching: false,
error: action.error
};
case FILTER_PRODUCTS_BY_CATEGORY_ID:
return {
...state,
data: filterProductsByCategoryId(state, action.categoryId)
};
default:
return state;
}
};
export default reducer;
And there is my code to call filter action:
filterProducts = (title = "A") => {
const _categories = Object.values(this.props.categories);
const selectedCategory = _categories.find(
category => category.title === title
);
this.props.dispatch(filterProductsByCategoryId(selectedCategory.id));
My questions is:
A) Is there is a way to filter my data and display them in UI and refresh them without using ACTIONS way??
B) If A's answer is No!, How can i get my state.data and filter them in FILTER_PRODUCTS_BY_CATEGORY_ID?
Thanks.
You can use the Array.prototype.filter() to return filtered result.
keep in mind that this will return an array and not a single value, which is a good thing if you are using this filter within your reducer. because your reducer's shape is an array and not an object.
Running example:
const myData = [{
name: 'some name',
id: 1
}, {
name: 'some name2',
id: 2
}, {
name: 'some name3',
id: 3
}, {
name: 'some name4',
id: 4
}]
const filterProductsByCategoryId = (state, action) => {
return state.filter(c => c.id === action.categoryId);
};
const result = filterProductsByCategoryId(myData, {categoryId: 2});
console.log(result);
I think it is more appropriate to create a selector for a singular product that will handle this kind of action, this way you will be able to return an object instead of an array with one product in it.
Not to mention the benefits of using reselect to do some memoizations.
For this task you can use the Array.prototype.find():
const myData = [{
name: 'some name',
id: 1
}, {
name: 'some name2',
id: 2
}, {
name: 'some name3',
id: 3
}, {
name: 'some name4',
id: 4
}]
const filterProductsByCategoryId = (state, id) => {
return state.find(c => c.id === id);
};
const result = filterProductsByCategoryId(myData, 2);
console.log(result);

Resources