How can I update an array in state? - reactjs

When my app loads I generate some initial data. My initial state is:
const INITIAL_STATE = { calendarData: [ ], index: 0 }
The data generated is an array of objects:
[
{
"date": "2017-09-24T16:13:24.419Z",
"id": 0,
"swiped": false,
"uri": "http://www.someurl.com/24.png",
},
{
"date": "2017-09-25T16:13:24.426Z",
"id": 1,
"swiped": false,
"uri": "http://www.someurl.com/25.png",
},
{
"date": "2017-09-26T16:13:24.426Z",
"id": 2,
"swiped": false,
"uri": "http://www.someurl.com/26.png",
}
]
I'm attempting to update calendarData like so:
return {...state, calendarData: [...state.calendarData, ...aboveArray]};
However, when I log mapStateToProps
const mapStateToProps = state => {
return { calendarData: state.calendarData };
};
Console shows the following:
Object {
"calendarData": Object {
"calendarData": Array [],
"index": 0,
},
I am trying to update the empty calendarData array in INITIAL_STATE with the new array of objects.
Before I made this change I was initializing state = [] and returning the aboveArray, which worked. Now I want my state to be an object that has a calendarData array and an index key,
state = {
calendarData: [],
index: 0,
}

Related

How can i change the value of an element in an object from extrareducer redux

This is the initial state:
const initialState ={
ordersWholesale:[
{
"id": 14,
"name": "XTPara 650mg Tablet",
"code": "XTP5656",
"date": "17/10/2022",
"accepted": null,
"wholesale": "shakthi",
"quantity": "5"
},
{
"id": 15,
"name": "Dolo 650 Tablet",
"code": "DOL1213",
"date": "17/10/2022",
"accepted": false,
"wholesale": "shakthi",
"quantity": "5"
},
],
}
This is the slice reducer
extraReducer: {
[asyncOrderAccept.fulfilled]: (state, { payload }) => {
}
}
How can I change only the value orderWholesale[0]['accepted']: true using the payload value which is 14?
If I'm understanding your question correctly that the action payload is the id of the ordersWholesale state element you want to toggle true, then you'll need to search the array to find the correct element by id and then update that element. Keep in mind that state is the ordersWholesale array and that Array.prototype.find potentially returns undefined if no match is found.
extraReducer: {
[asyncOrderAccept.fulfilled]: (state, { payload }) => {
const el = state.find(order => order.id === payload);
if (el) {
el.accepted: true,
}
},
}
This may also work for you if you can use Optional Chaining.
extraReducer: {
[asyncOrderAccept.fulfilled]: (state, { payload }) => {
state.find(order => order.id === payload)?.accepted = true;
},
}

Global state with redux somehow switches variables

I am very confused as to why this is happening as this has never happened with me before using redux. I am building a react native application and currently when I try to console log store.getStore() get the following output.
Object {
"userState": Object {
"currentUser": Object {
"email": "test120#gmail.com",
"username": "test13",
},
"listings": Array [],
},
}
Now, when I dispatch my fetchUserListings() action, which should update the listings in the state the following happens.
Object {
"userState": Object {
"currentUser": Array [
Object {
"addressData": Object {
"description": "2300 Yonge Street, Toronto, ON, Canada",
"matched_substrings": Array [
Object {
"length": 3,
"offset": 0,
},
],
"place_id": "ChIJx4IytjwzK4gRwIPk2mqEJow",
"reference": "ChIJx4IytjwzK4gRwIPk2mqEJow",
"structured_formatting": Object {
"main_text": "2300 Yonge Street",
"main_text_matched_substrings": Array [
Object {
"length": 3,
"offset": 0,
},
],
"secondary_text": "Toronto, ON, Canada",
},
"terms": Array [
Object {
"offset": 0,
"value": "2300",
},
Object {
"offset": 5,
"value": "Yonge Street",
},
Object {
"offset": 19,
"value": "Toronto",
},
Object {
"offset": 28,
"value": "ON",
},
Object {
"offset": 32,
"value": "Canada",
},
],
"types": Array [
"street_address",
"geocode",
],
},
"addressDescription": "2300 Yonge Street, Toronto, ON, Canada",
"bath": "6",
"benefits": Array [
"Large Beds",
"Nearby Bustop",
"In-building gym",
],
"urls": Array [
"https://firebasestorage.googleapis.com/v0/b/studenthousingfinder-11f55.appspot.com/o/listing%2FoHr0OMukEFguYxJborrvMAJQmre2%2F0.bd7cwka5gj?alt=media&token=81b3e06a-65a9-44a7-a32d-d328014058e7",
"https://firebasestorage.googleapis.com/v0/b/studenthousingfinder-11f55.appspot.com/o/listing%2FoHr0OMukEFguYxJborrvMAJQmre2%2F0.k78etqzypk?alt=media&token=e2622547-00f4-447b-8bea-799758734f0d",
],
},
],
"listings": Array [],
},
}
Basically the API call is working and the state is updated, however somehow the data sent back is updating the currentUser in the state rather than the listings.
Here is my current reducer code:
import {USER_LISTINGS_STATE_CHANGE, USER_STATE_CHANGE} from '../constants';
const initialState = {
currentUser: null,
listings: [],
};
export const userReducer = (state = state || initialState, action) => {
switch (action.type) {
case USER_STATE_CHANGE:
return {
listings: state.listings,
currentUser: action.payload,
};
case USER_LISTINGS_STATE_CHANGE:
return {
currentUser: state.currentUser,
listings: action.payload,
};
default:
return state;
}
};
and here are the 2 functions I use to make the API request
export function fetchUser() {
return async (dispatch) => {
db.collection('users')
.doc(auth.currentUser.uid)
.get()
.then((snapshot) => {
if (snapshot.exists) {
console.log('Yo');
dispatch({type: USER_STATE_CHANGE, payload: snapshot.data()});
} else {
console.log('Does not exist');
}
});
};
}
export function fetchUserListings() {
return async (dispatch) => {
db.collection('posts')
.doc(auth.currentUser.uid)
.collection('userListings')
.orderBy('title', 'desc')
.get()
.then((snapshot) => {
let listingArr = snapshot.docs.map((doc) => {
const data = doc.data();
const id = doc.id;
return {id, ...data};
});
dispatch({
type: USER_LISTINGS_STATE_CHANGE,
payload: listingArr,
});
});
};
}
Any help would be appreciated as I'm really lost as to why this is happening!
It seems like that your users collection in database might be having wrong entry.
Fact that brought me to this conclusion is that in your fetchUserListings() function, you're actually adding an extra field to json object i.e id.
Now, the console.log output doesn't contain this id field which could only mean that fetchUserListings() is not the one being called here.
You can try putting some try catch block and console.log('fetchUser', snapshot.data()) & console.log('fetchUserListings', snapshot.docs) in respective functions to see which one is being called.

Update single record in nested state object, react-redux

I am working on a grid structure where user can add sections, sub-sections or items dynamically. I am managing that things in my redux state object. UI of my grid is as following :
I want to update a single row record instead of reloading whole grid again. For that, whenever user changes any cell value of row i am calling update-row api and on success of that i am trying to update that value in reducer using following code.
case UPDATE_ORDER_LINES_SUCCESS:
let stateData = state.get(`GridData`);
const dataIndex = stateData.children.findIndex(
(listing) => listing.id === action.row.id // row id which is updated
);
stateData[0].children[dataIndex] = action.row;
let data = Object.assign(stateData, { children: stateData.children });
state = state.set(`GridData`, [data]);
This code is working fine for first level of children records (as per json object) but problem occur if user update value of nth level children record. How can i update that row record in my redux state ?
My current redux state sample is :
{
"views": [
{
"id": "5e6b8961ba08180001a10bb6",
"viewName": "house",
"description": "house view",
"name": "house",
"children": [
{
"id": "5e6b8961ba08180001a10bb7",
"viewName": "house",
"sectionName": "Temporary",
"sectionId": "SEC-02986",
"description": "Temporary",
"sequenceNumber": 4,
"refrenceId": "SEC-02986",
"children": [
{
"id": "5e590df71bbc71000118c109",
"lineDescription": "AutoPickPack01",
"lineAction": "Rent",
"quantity": 5,
"deliveryDate": "2020-02-29T06:00:00+11:00",
"pickDate": "2020-02-28T06:00:00+11:00",
"pickupDate": "2020-03-01T06:00:00+11:00",
"prepDate": "2020-02-28T06:00:00+11:00",
"returnDate": "2020-03-01T06:00:00+11:00",
"shippingDate": "2020-02-29T06:00:00+11:00",
"unitPrice": 7000,
"children": [
{
"id": "5e590df71bbc71000118c10a",
"orderId": "Ord-05788_1",
"lineNumber": "01a7b77c-792a-4edb-9b73-132440621968",
"purchaseOrderNumber": null,
"lineDescription": "29Janserial",
"lineAction": "Rent",
"quantity": 5,
"pricingMethod": "Fixed",
"displayUnit": "Days",
"unitPrice": 0,
"chargeAmount": 0,
"pickDate": "2020-02-17T06:00:00+11:00",
"prepDate": "2020-02-28T06:00:00+11:00",
"shippingDate": "2020-02-29T06:00:00+11:00",
"deliveryDate": "2020-02-29T06:00:00+11:00",
"pickupDate": "2020-03-01T06:00:00+11:00",
"returnDate": "2020-03-01T06:00:00+11:00",
"name": "29Janserial",
"description": "29Janserial",
"discountAmount": "",
"discountPrice": ""
}
]
}
]
}
]
}
]
}
What is the best way to update nested children row data in reducer ?
As redux doesn't allow to mutate the current state and return it back, it's hard to modify a nested child. Although its highly discouraged to
have this kind of nested structure in redux, rather it should be normalized as #bsapaka answered. But if you still want to update the nested
object and return the whole state as an immutable one, immer should be your friend. immerJS has been so popular for handling immutable states.Although
Install immer and redux-immer in your case
yarn add immer redux-immer
In your reducers.js file where all reducers have been combined using combineReducers
import produce from 'immer';
import { combineReducers } from 'redux-immer';
// Replace your current combineReducers with
combineReducers(produce, { /* Object of all reducers */ });
In your current reducer file
import product from 'immer';
const findNestedChild = (arr, itemId) => (
arr.reduce((a, item) => {
if (a) return a;
if (item.id === itemId) return item;
if (item['children']) return findItemNested(item['children'], itemId)
}, null)
);
case UPDATE_ORDER_LINES_SUCCESS:
return produce(state, draftState => {
const { row: newChild, row: { id }} = action;
let child = findNestedChild(draftState.views, id);
child = newChild;
});
You should normalize your state, which flattens the tree, and entities become associated by id references instead of direct nesting.
For example
{
"entities": {
"orders": {
"o1": { "id": "o1", "productIds": ["p1", "p2"] },
"o2": { "id": "o2", "productIds": ["p2", "p3"] },
"o3": { "id": "o2", "productIds": ["p3"] }
},
"products": {
"p1": { "id": "p1", "orderIds": ["o1"] },
"p2": { "id": "p1", "orderIds": ["o1", "o2"] },
"p3": { "id": "p1", "orderIds": ["o2", "o3"] }
},
"views": {
"v1": { "id": "v1", "childIds": ["v1.1", "v1.2"] },
"v1.1": { "id": "v1.1", "parentId": "v1" },
"v1.2": { "id": "v1.2", "parentId": "v1" }}
},
"ids": {
"orders": ["o1", "o2", "o3"],
"products": ["p1", "p2", "p3"],
"views": ["v1", "v1.1", "v1.2"]
}
}
There's more upfront work of finding the correct model and transforming the raw data into it, but you save a lot of time not having to deal with updates that are nested or affect multiple areas of data.
Redux docs on normalizing
A (de)normalization transformation tool
A reducer utility library to manage normalized state

React: setState with spead operator seems to modify state directly

I am creating a page in React to filter attributes that are defined in my state with a isChecked like so:
this.state = {
countries: [
{ "id": 1, "name": "Japan", "isChecked": false },
{ "id": 2, "name": "Netherlands", "isChecked": true },
{ "id": 3, "name": "Russia", "isChecked": true }
//...
],
another: [
{ "id": 1, "name": "Example1", "isChecked": true },
{ "id": 2, "name": "Example2", "isChecked": true },
{ "id": 3, "name": "Example3", "isChecked": false }
//...
],
//... many more
};
I am creating a function resetFilters() to set all the isChecked to false in my state:
resetFilters() {
// in reality this array contains many more 'items'.
for (const stateItemName of ['countries', 'another']) {
// here i try to create a copy of the 'item'
const stateItem = [...this.state[stateItemName]];
// here i set all the "isChecked" to false.
stateItem.map( (subItem) => {
subItem.isChecked = false;
});
this.setState({ stateItemName: stateItem });
}
this.handleApiCall();
}
My problem is: it seems I am directly modifying state, something that is wrong, according to the docs. Even though my function seems to work, when I remove the line this.setState({ stateItemName: stateItem }); it will also seem to work and when I console log stateItem and this.state[stateItemName] they are always the same, even though I am using the spread operator which should create a copy. My question: how is this possible / what am I doing wrong?
That is because the spread syntax does only shallow copying. If you want to carry out deep copying, you should also be spreading the inner objects within each array.
for (const stateItemName of ['countries', 'another']) {
const stateItem = [...this.state[stateItemName]];
const items = stateItem.map( (subItem) => ({
...subItem,
isChecked: false,
}));
this.setState({ [stateItemName]: items });
}
I think your code could be reduced more so, for example an approach could be:
function resetFilters() {
const targetItems = ['countries', 'another'];
const resetState = targetItems.reduce((acc, item) => ({
...acc,
[item]: this.state[item].map(itemArray => ({
...itemArray,
isChecked: false
}))
}), {})
this.setState(state => ({ ...state, ...resetState }), this.handleApiCall);
}
The benefit here is that the api call is done after state is updated. While updating current state correctly.
Let me know how it works out 👌🏻
-Neil

React setState - Add array to nested object with multiple arrays

I'm currently working on a new application in React. This is the first time I'm creating something in React. The application will display our own promotions.
My initial state is as follows:
{
"promotion": {
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [
{
"startDateTimeStamp": 1510558814960,
"endDateTimeStamp": 1510558814960,
"variants": [
{
"title": "",
"text": "",
"image": ""
}
]
}
]
}
}
This is created from my defaultPromotion constant. This constant is stored in a separate file, which I call api.js
export const defaultPromotion = {
name: '',
campaign: '',
url: 'https://',
position: 0,
periods: [
{
startDateTimeStamp: Date.now(),
endDateTimeStamp: Date.now(),
variants: [
{
title: '',
text: '',
image: '',
},
]
},
]
}
In my createPromotion component it's created as followed
let promotionState = api.promotions.defaultPromotion;
this.state = {
promotion: promotionState
};
I can add a new period with the following:
addPromotion() {
let promotion = this.state.promotion;
promotion.periods.push( api.promotions.defaultPromotion.periods[0] );
this.forceUpdate();
}
After that, a new period is added as expected. Suggestions to do this with setState() are very welcome! So, my new state is now:
{
"promotion": {
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [
{
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [
{
"title": "",
"text": "",
"image": ""
}
]
},
{
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [
{
"title": "",
"text": "",
"image": ""
}
]
}
]
}
}
Now, I want to add a new variant for this promotion period, this is where I'm stuck for 2 days now.
I'm adding a new period as follows:
addVariant( periodKey ) {
const promotion = this.state.promotion;
promotion.periods[periodKey].variants.push(api.promotions.defaultPromotion.periods[0].variants[0]);
this.setState({ promotion: promotion });
}
periodKey is here "1", so, I'm expecting that there will be added a new variant for periods[1], but, it's added to both periods. State is now as follows:
{
"promotion": {
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [
{
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [
{
"title": "",
"text": "",
"image": ""
},
{
"title": "",
"text": "",
"image": ""
}
]
},
{
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [
{
"title": "",
"text": "",
"image": ""
},
{
"title": "",
"text": "",
"image": ""
}
]
}
]
}
}
Can someone explain me why this is happening and how I can add a new variant the right way?
Many, many thanks in advance!
UPDATE 1
Based on the answers from bennygenel and Patrick Hübl-Neschkudla, my implementation is now as follows:
Setting the initial state:
constructor(props) {
super(props);
let promotionState = api.promotions.defaultPromotion;
this.state = { ...promotionState };
}
Method:
addVariant( periodKey ) {
this.setState((prevState) => {
const { periods } = prevState;
periods[periodKey].variants.push(
Object.assign({}, { ...periods[periodKey].variants, api.promotions.defaultPromotion.periods[0].variants[0]})
);
return { periods };
});
}
But this still is setting the new variant in all the periods. I've also tried the exact code from Benny, but with the same results. The method is called as
this.props.addVariant( this.props.periodKey );
Even when I call it as:
this.props.addVariant(2);
The same behaviour is happening.
UPDATE 2
I now have rewritten everything to redux, this is so I have access to my promotion in every component the easy way, instead off passing them through certain components. Based on the answer of #mersocarlin, I now have the following reducer cases:
Add period
case PROMOTION_ADD_PERIOD:
const { periods } = { ...state };
periods.push(api.promotions.defaultPromotion.periods[0]);
state = {
...state,
periods: periods
};
break;
Add a period variant
case PROMOTION_ADD_PERIOD_VARIANT :
state = {
...state,
periods: [
...state.periods[action.payload.period],
{
variants: [
...state.periods[action.payload.period].variants,
api.promotions.defaultPromotion.periods[0].variants[0]
]
}
]
};
break;
The following case:
Add a new variant, works, state:
{
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [
{
"startDateTimeStamp": 1510599968588,
"endDateTimeStamp": 1510599968588,
"variants": [
{
"title": "",
"text": "",
"image": ""
}
]
},
{
"startDateTimeStamp": 1510599968594,
"endDateTimeStamp": 1510599968594,
"variants": [
{
"title": "",
"text": "",
"image": ""
}
]
}
]
}
After that, adding a new variant, kinda works, well, the variant is added, but I'm losing my 2nd period. State:
{
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [
{
"variants": [
{
"title": "",
"text": "",
"image": ""
},
{
"title": "",
"text": "",
"image": ""
}
]
}
]
}
I think this is a small thing I'm not see'ing. Does someone have the solution for the "PROMOTION_ADD_PERIOD_VARIANT" case?
Update 3
Changed the "PROMOTION_ADD_PERIOD" case as follows:
case PROMOTION_ADD_PERIOD:
state = {
...state,
periods: [
...state.periods,
initialState.periods[0]
]
};
break;
Update 4
Finaly found the solution. See the final code for PROMOTION_ADD_PERIOD_VARIANT below:
state = {
...state,
periods: [
...state.periods.map((item, index) => {
if ( index !== action.payload.period ) {
return item;
}
return {
...item,
variants: [
...item.variants,
initialState.periods[0].variants[0]
]
}
})
]
};
Thank you all so much for your help!!
Rather destruct your state object and avoid mutating it directly. This also happens to be a bad pattern.
Whenever you need to add a new item to the array:
const state = {
arrayProp: [{ prop1: 'prop1', prop2: 'prop2' }]
}
const newItem = {
prop1: 'value1',
prop2: 'value2',
}
const newState = {
...state,
arrayProp: [
...state.arrayProp,
newItem,
]
}
console.log('newState', newState)
Same applies for nested properties within your state:
Redux also uses this very same approach
const state = {
objectProp: {
arrayPropWithinArray: [
{ id: '0', otherProp: 123, yetAnotherProp: 'test' },
{ id: '1', otherProp: 0, yetAnotherProp: '' }
]
}
}
const { objectProp } = state
const index = objectProp.arrayPropWithinArray.findIndex(obj => obj.id === '1')
const newSubItem = {
otherProp: 1,
yetAnotherProp: '2',
}
const newState = {
...state,
objectProp: {
...objectProp,
arrayPropWithinArray: [
...objectProp.arrayPropWithinArray.slice(0, index),
{
...objectProp.arrayPropWithinArray[index],
...newSubItem,
},
...objectProp.arrayPropWithinArray.slice(index + 1),
]
}
}
console.log('newState', newState)
Your specific case (as described in your comment)
const periodKey = '2' // your periodKey var. Get it from the right place, it can be your action for example
const index = state.periods.findIndex(period => period.id === periodKey) // find which index has to be updated
state = {
...state, // propagates current state
periods: [
...state.periods.slice(0, index), // propagates everything before index
{
...state.periods[index],
variants: [
...state.periods[index].variants,
api.promotions.defaultPromotion.periods[0].variants[0],
],
},
...state.periods.slice(0, index + 1) // propagates everything after index
]
}
So, what's happening here is that you have an array with two references to the same object.
Imagine it like this:
myArray[0] = reference to defaultPromotion
myArray[1] = reference to defaultPromotion
That's actually a wonderful example of why immutability concepts got so much attention in the past few years :)
What you'd want to do here is instead of adding the defaultPromotion object to the promotions array, you create a new object with the same props as this object and add it. It would look something like this (depending on your ES version etc.)
promotion.periods.push(
Object.assign({}, api.promotions.defaultPromotion.periods[0])
);
This way, you're creating a new object and pass this to the array instead of a reference to the already existing one.
First suggestion, if you are going to have only one promotion object in your state and not an array, lose the promotion level. this will reduce the complexity of your state. You can use spread syntax to easily set your initial state.
Example
let promotionState = api.promotions.defaultPromotion;
this.state = { ...promotionState };
Above code would end up creating a state like below;
{
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [{
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [{
"title": "",
"text": "",
"image": ""
}]
}, {
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [{
"title": "",
"text": "",
"image": ""
}]
}]
}
Another suggestion I can make is to use functional setState to reduce possibility to mutate.
Example
addPromotion() {
this.setState((prevState) => {
const { periods } = prevState;
periods.push(api.promotions.defaultPromotion.periods[0]);
return { periods };
});
}
addVariant( periodKey ) {
this.setState((prevState) => {
const { periods } = prevState;
periods[periodKey].variants.push(api.promotions.defaultPromotion.periods[0].variants[0]);
return { periods };
});
}

Resources