How can I dynamically write a reducer (useReducer hook)? - javascript

I am trying to write a dynamic reducer to loop over a list and setting each item in this list a counter.
I am not sure I am doing it right - mainly in the section where I set it a value of ' ' (and can't dynamically name it or set the value I want initially (each will have a different value))
const reducer = (state, action) => {
switch(action.type) {
case 'SET_COUNTER':
return {
...state,
[`counter${action.id}`]: action.payload
}
default:
return state
}
}
//is the below correct?
let [{ counter }, dispatchReducer] = useReducer(reducer, {
counter: '',
})
I am then looping over an array of objects to create different counters (e.g. counter0, counter1, counter2 ...) and set each of them a value
//this dispatch is not working
useEffect(() => {
availableTimes.map(item =>
dispatchReducer({
type: 'SET_COUNTER',
id: item.id,
payload: counts[`${item.time}`]
})
)
}, [])
The payload comes from an object which I am using to count the instances of a time. E.g. if "2230" appears 3 times, this object will have "2230": 3
const counts = {}
extractedTiesm.forEach(x => {
counts[x] = (counts[x] || 0) + 1
})
//console.log(counts["2230"]) --> 3

Related

Redux Toolkit state object is not changing "Uncaught TypeError: Cannot assign to read only property 'property-name' of object '#<Object>'"

I want to change the data of a state object but Redux Toolkit is not changing and gives an error like Uncaught TypeError: Cannot assign to read only property 'status' of object '#<Object>'.
These lines from component:
const [width, setWidth] = useState(8)
const [height, setHeight] = useState(9)
const [table, setTable] = useState({
rows:
[...Array(height)].map(()=>
(
{
cells:
[...Array(width)].map(()=>
(
{status: true}
)
)
}
)
)
})
useEffect(()=>{
dispatch(changePlayableFields(table)) // <- it's not changing the state
},[table])
function changeCell(i:number,k:number){
const localTable = {...table}
localTable.rows[i].cells[k].status = !localTable.rows[i].cells[k].status // <-Uncaught TypeError: Cannot assign to read only property 'status' of object '#<Object>'
setTable(localTable)
}
changeCell function is working very well and I see the truth results on the page. But when adding useEffect codes to move new datas to keep them in memory with redux, then I get the errors.
And these are from Redux Slice:
import { createSlice } from "#reduxjs/toolkit"
const levelSlice = createSlice({
name: "level",
initialState: {
gridSizeAndPlayableFields: {
width: 8,
height: 9,
playableFields: {
rows:
[...Array(9)].map(()=>
(
{
cells:
[...Array(8)].map(()=>
(
{status: true}
)
)
}
)
)
}
},
},
reducers: {
changePlayableFields: (state, action) => {
state.gridSizeAndPlayableFields.playableFields = action.payload // <- it's not changing the data
},
}
})
export const {changeGridSize, changePlayableFields} = levelSlice.actions
export default levelSlice.reducer
It's little about my previous question, maybe you'd like to check it. Here is my previous question link: Redux Slice is returning an error after adding a function into a property of initial states
I hope anyone can help. Thanks...
1. Reducer does not update issue:
It seems you are sending rows as payload of the dispatch. So you should update gridSizeAndPlayableFields.playableFields.rows in the reducer:
changePlayableFields: (state, action) => {
// console.log(action.payload);
state.gridSizeAndPlayableFields.playableFields.rows = action.payload.rows;
// console.log(current(state.gridSizeAndPlayableFields.playableFields));
},
2. object update issue in changeCell method:
In a normal function you cannot mutate the object like in changeCell function. You can only do this in the redux toolkit slice thanks to Immer.
You can map the rows and cells arrays to update the corresponding indeces' status values. You can write the changeCell method in the following way:
function changeCell(i, k) {
if (!table) return;
const localTable = {};
const localTableRows = [...table.rows];
// map rows array
const updatedRows = localTableRows.map((item, index) => {
// if index equals i, map its cells as well
//... and find kth cell and change its status
if (index === i) {
return {
...item,
cells: item.cells.map((c, idx) => {
if (idx === k) {
return {
...c,
status: !c.status,
};
}
// if idx not equals to k return old cell item
return c;
}),
};
}
// if index is not equal i return old row item
return item;
});
localTable.rows = updatedRows;
setTable(localTable);
}
I suppose i and k index values for the zero based array.
You can use Object.reduce method to calculate updated table object in your changeCell function; but personally I try to avoid from this because in general Object.reduce is less performant.

How do I set the value of a property via a function?

I'm trying to run a function inside a reducer function to calculate a cart total, but the value in the state object is the function and not the result of the function. After render, the result is displayed, but I cannot pass the object to other components (I'm using a context). How do I do it? Here is the code (assume that the values work, because they do).
I've tried setting it to the const to no avail. I've tried an anonymous function that returns the function's total, and it still doesn't work. I've also tried just calling the function.
const reducer = (cart, action) => {
switch(action.type) {
case("ADD_ITEM"):
return {
...cart,
products: {
...cart.products,
[action.payload.product.id]: {...action.payload.product}
},
total: () => (cartTotal)
}
break
case("REMOVE_ITEM"):
delete cart.products[action.payload]
return {
...cart,
products: {
...cart.products
},
total: () => (cartTotal)
}
break
case("CLEAR_CART"):
return {
cart: {
...initialState
}
}
break
}
}
Here is the object:
cart: {
products: [{}],
total: 0
}
Here is the function to return the total:
const cartTotal = () => {
const total = Object.values(cart.products).reduce((prev, curr) => {
const currPrice = (curr.data.on_sale && curr.data.sale_price) ? curr.data.sale_price : curr.data.price
return prev + currPrice
}, 0)
return total.toFixed(2)
}
Right now, I'm passing the method that allows you to calculate the total, but it seems like it is unnecessary, as I'm watching the cart state and updating the value of total each time items are added/removed. How do I set the value of a property inside of the reducer function as the return of another helper function? Thanks!
I don't know why are you storing derived state, well, in state? This should be computed via a selector when reading your state out (and/or passed to a custom Context provider).
If you must store the total in state then you need to call the cartTotal function to be able to store its return value. Unfortunately this will only compute the cart total on the unupdated cart since you are currently in the function that returns the new cart state.
You can factor out the cart update so you have an updated cart products object, and with a small revision of cartTotal it can consume this updated cart products object and compute a total.
Example:
const cartTotal = (products) => {
const total = Object.values(products).reduce((prev, curr) => {
const currPrice = (curr.data.on_sale && curr.data.sale_price) ? curr.data.sale_price : curr.data.price;
return prev + currPrice;
}, 0);
return total.toFixed(2);
};
Cases
case "ADD_ITEM": {
const { payload } = action;
const products = {
...cart.products,
[payload.product.id]: { ...payload.product },
}
return {
...cart,
products,
total: cartTotal(products),
}
break;
}
case "REMOVE_ITEM": {
const products = { ...cart.products };
delete products[action.payload];
return {
...cart,
products,
total: cartTotal(products),
}
break;
}
You need to call cartTotal to calculate the value.
total: cartTotal()

Adding key value pairs to redux store

I am trying to use redux to add key value pairs to my store. However, Im not sure how to accomplish this. In short, i am retrieving data from firebase, I want to add that data to my redux store, but I have to do it one item at a time. My desired structure of my state object is something like this:
reminders
- reminder key 1
- reminder title
- reminder date 1
- reminder key 2
- reminder title
- reminder date 1
and so on.
But I cant figure out how to add children to my state.reminders object
Here is my action:
const fetchReminders = (uid) => async dispatch => {
firebaseReminders.child(uid).orderByChild("date").on("value", snapshot => {
snapshot.forEach(function(child) {
console.log(child.val())
dispatch({
type: 'fetchReminders',
value: child.val(),
key: child.key
});
})
});
};
so this would dispatch the action for every single item that I retrieve from the database, and then in my reducer I want to add that item to the state tree using action.key as the key. Currently I have
const remindersReducer = (state = initialState, action) => {
switch(action.type) {
case "fetchReminders":
return Object.assign({}, state, {
reminders: action.value
});
default: return state;
}
};
which is not correct. How can I add a child node to my state.reminders object with the key of action.key, and the value of action.value
let initialState = {
reminders: {}
}
const remindersReducer = (state = initialState, action) => {
switch(action.type) {
case "fetchReminders":
return Object.assign({}, state, {
reminders: {
...state.reminders,
[action.key]: action.value
}
});
default: return state;
}
};
let state1 = remindersReducer(initialState, {
type: 'fetchReminders',
key: 'reminderKey1',
value: 'reminderValue1'
});
console.log(state1)
let state2 = remindersReducer(state1, {
type: 'fetchReminders',
key: 'reminderKey2',
value: 'reminderValue2'
});
console.log(state2)
let state3 = remindersReducer(state2, {
type: 'fetchReminders',
key: 'reminderKey3',
value: 'reminderValue3'
});
console.log(state3)
The snippet should help you achieve what you want to do.
You can assign an object as the key of action.key by using the following format:
{
[action.key]: action.value
}
Its called Computed Property Names.
Starting with ECMAScript 2015, the object initializer syntax also
supports computed property names. That allows you to put an expression
in brackets [], that will be computed and used as the property name.
Source

Reducer updated with wrong value(array got updated with one item with few items inside instead spreading them)

i'm having hard time figure out this. Have component which is search filter and pushes all selected filters into url. Everything works like it should except in case of refresh, in that case reducer is updated for selected filter with array with single item in which i have all selected items, not spreaded into array.
f.e. i have url
myexampleapp.com/alltrips?tripType=short_walk,cycling,downhill_cycling,long_walks&season=spring,summer,alle,vinter&lengthTo=50
my reducer
// ------------------------------------
// Constants
// ------------------------------------
export const UPDATE_FILTERS = 'UPDATE_FILTERS';
// ------------------------------------
// Actions
// ------------------------------------
const updateFilter = (key, value) => ({
type: UPDATE_FILTERS,
payload: {
key,
value
}
});
// ------------------------------------
// Action creators
// ------------------------------------
export const updateFilterState = (key, value) => {
return dispatch => {
dispatch(updateFilter(key, value));
};
};
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
tripType: [],
season: [],
tripsTo: undefined,
tripsFrom: undefined
};
export function filterReducer (state = initialState, action) {
switch (action.type) {
case UPDATE_FILTERS: {
const key = action.payload.key;
const value = action.payload.value;
if (key === 'tripsFrom' || key === 'tripsTo') {
return Object.assign({}, state, { [key]: value });
} else {
var newFilter = state[key].slice();
var ttIdx = state[key].indexOf(value);
if (ttIdx !== -1) {
newFilter.splice(ttIdx, 1);
} else {
newFilter.push(value);
}
}
console.log(newFilter);
return Object.assign({}, state, { [key]: newFilter });
}
default:
return state;
}
}
console.log returns array with 1 element in which have array with 5 elements. but i want that 5 ekements to be in parrent array.
and i'm parsing URL
componentDidMount () {
let {
location: { search },
updateFilterState
} = this.props;
search = search.slice(1);
var queries = search.split('&');
queries.forEach(q => {
var tmp = q.split('=');
if (tmp[0] && tmp[1]) {
if (tmp[0].toLowerCase() === 'triptype') {
updateFilterState(tmp[0], tmp[1].split(','));
console.log(tmp[1].split(','));
} else if (tmp[0].toLowerCase() === 'tripsto') {
updateFilterState(tmp[0], tmp[1]);
} else if (tmp[0].toLowerCase() === 'tripsfrom') {
updateFilterState(tmp[0], tmp[1]);
} else if (tmp[0].toLowerCase() === 'season') {
updateFilterState(tmp[0], tmp[1].split(','));
}
}
});
this.updateQuery(this.props);
}
So everything works except when i want to refresh.
Pretty new with all this, and been stuck for almost 3 days with this. Hope you understand what im trying to ask here as i'm pretty new and non-english speaker, so i don't know all the terms so i can better express myself. Can someone give me some pointers?
If I'm not mistaken you are feeding the reducer with an array for season and tripType. So, when you try to update those values, you are not actually spreading that array. This is your value parameter. Hence, if you do this you will have a parent array with your desired result:
newFilter.push(...value);
... is ES6's spread syntax. So we are spreading our array and pushing it into our newFilter.
But again if I don't see it wrong you will have problems with this code since you are not checking the existence of your values right. You are looking indexOf something but if you really feeding your reducer with an array, for which one you are looking this index?
Here is a cleaner way of doing this if I'm not mistaken what you are trying to do here:
export function filterReducer (state = initialState, action) {
switch (action.type) {
case UPDATE_FILTERS: {
const { key, value } = action.payload;
if (key === 'tripsFrom' || key === 'tripsTo') {
return { ...state, [key]: value };
}
const newFilter = Array.isArray(value)
? [ ...new Set( [ ...state[key], ...value ] ) ]
: [ ...new Set( [ ...state[key], value ] ) ];
return { ...state, [key]: newFilter};
}
default:
return state;
}
}
Some differences with your code:
I am using spread syntax instead of Object.assign.
Instead of checking all the existence values (iterating the array and doing some logic) I'm using here Set object. It creates an object of unique values of what we give it. So I am cheating here and spreading our old state with spreading our value into an array, give this to our Set, and again at the top level spreading it again into an array. If you don't do the last spread you will get an object but here we want an array.

state.findIndex is not a function error with findIndex

I'm passing the id of the object as the action.payload to the reducer to modify the object.
Reducer:
const initialState = {
posts: [
{id:1, name:'post1', number:11},
{id:2, name:'post2', number:22},
{id:3, name:'post3', number:33}
]
}
export default function newData (state = initialState, action) {
switch (action.type) {
case "updateNumber": {
// find object to update
const index = state.findIndex(({ id }) => id == action.payload);
if (index > -1 ) {
const toIncrement = state[index];
const number = toIncrement.number++;
// create new object with existing data and newly incremented number
const updatedData = { ...toIncrement, number };
// return new array that replaces old object with incremented object at index
return [...state.slice(0, index), updatedData, ...state.slice(index + 1)];
}
// return state if no object is found
return state;
}
default:
return state
}
}
But I'm getting error: state.findIndex is not a function. How to find the index of the element in the posts array? console.log actions is giving me {type: "updateNumber", payload: 2} where payload is the element pressed.
UPDATE1:
export default function newData (state = initialState, action) {
switch (action.type) {
case "updateNumber": {
// find object to update
const index = state.posts.findIndex(({ id }) => id == action.payload);
if (index > -1 ) {
const toIncrement = state.posts[index];
const number = toIncrement.posts.number++;
// create new object with existing data and newly incremented number
const updatedData = { ...toIncrement, number };
// return new array that replaces old object with incremented object at index
return [...state.posts.slice(0, index), updatedData, ...state.posts.slice(index + 1)];
}
// return state if no object is found
return state;
}
default:
return state
}
}
So this is supposed to return the posts with updated number in the state, right?
Your initialState is an object.
I think you meant
state.posts.findIndex(({ id }) => id == action.payload);
Or maybe change the initialState to
const initialState = [
{id:1, name:'post1', number:11},
{id:2, name:'post2', number:22},
{id:3, name:'post3', number:33}
]
Edit
As a followup to your edit,
After your change, Now you can do:
const number = toIncrement.number++;
As totalIncrement will hold an object like this for example:
{id:1, name:'post1', number:11}
Edit #2
I think you are mutating the state which is not allowed in redux.
Try changing this:
if (index > -1 ) {
const toIncrement = state.posts[index];
const number = toIncrement.posts.number++;
To this:
if (index > -1 ) {
const toIncrement = {...state.posts[index]};
const number = toIncrement.posts.number + 1; // i hope this is a number and not a string!
Another thing, Your initial state is an object but your reducer returns an array.
Change this line:
// return new array that replaces old object with incremented object at index
return [...state.posts.slice(0, index), updatedData, ...state.posts.slice(index + 1)];
To this line:
// return new array that replaces old object with incremented object at index
return { posts: [...state.posts.slice(0, index), updatedData, ...state.posts.slice(index + 1)]};

Categories