ReactJS: Updating array inside object state doesn't trigger re-render - javascript

I have a react hooks function that has a state object apiDATA. In this state I store an object of structure:
{
name : "MainData", description: "MainData description", id: 6, items: [
{key: "key-1", name : "Frontend-Test", description: "Only used for front end testing", values: ["awd","asd","xad","asdf", "awdr"]},
{key: "key-2", name : "name-2", description: "qleqle", values: ["bbb","aaa","sss","ccc"]},
...
]
}
My front end displays the main data form the object as the headers and then I map each item in items. For each of these items I need to display the valuesand make them editable. I attached a picture below.
Now as you can see I have a plus button that I use to add new values. I'm using a modal for that and when I call the function to update state it does it fine and re-renders properly. Now for each of the words in the valuesI have that chip with the delete button on their side. And the delete function for that button is as follows:
const deleteItemFromConfig = (word, item) => {
const index = apiDATA.items.findIndex((x) => x.key === item.key);
let newValues = item.value.filter((keyWord) => keyWord !== word);
item.value = [...newValues];
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
apiDATA.items = [...apiDataItems];
setApiDATA(apiDATA);
}
});
};
Unfortunately this function does not re-render when I update state. And it only re-renders when I update some other state. I know the code is a bit crappy but I tried a few things to make it re-render and I can't get around it. I know it has something to do with React not seeing this as a proper update so it doesn't re-render but I have no idea why.

It is not updating because you are changing the array items inside apiDATA, and React only re-render if the pointer to apiDATA changes. React does not compare all items inside the apiDATA.
You have to create a new apiDATA to make React updates.
Try this:
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
setApiDATA(prevState => {
return {
...prevState,
items: apiDataItems
}
});
}

Using splice isn't a good idea, since it mutates the arrays in place and even if you create a copy via let apiDataItems = [...apiDATA.items];, it's still a shallow copy that has original reference to the nested values.
One of the options is to update your data with map:
const deleteItemFromConfig = (word, item) => {
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
const items = apiDATA.items.map(it => {
if (it.key === item.key) {
return {
...item,
values: item.value.filter((keyWord) => keyWord !== word)
}
}
return item;
})
setApiDATA(apiData => ({...apiData, items});
}
});
}

Related

How to flat array in zustand

I have a brands list array, every brand object has a brand name & products
So the first time, I pushed the new brand object to the brands list, and that's work well,
Now if I want to push a new brand to the brands list and keep the previous brand data it does not work as expected.
and it looks like this
the expected should be like this
I'm trying to use flat() but not work here
minimal reproduction here
Code
import create from 'zustand';
import {persist} from 'zustand/middleware';
import {zustandStorage} from '../utils/Storage';
export const useFavoritesStore = create(
persist(
set => ({
brandsFavoritesList: [],
updateFavList: brandData => {
set(state => ({
brandsFavoritesList:
// check if there are any brands on the list before
state.brandsFavoritesList.length > 0
? state.brandsFavoritesList.map(brand =>
// if the brand exists before pushing new products
brand.tenant === brandData?.tenant
? {
...brand,
products: [...brand.products, ...brandData.products],
}
: // if the brand does not exist before push the new brand data to the brandsFavoritesList
// Issue here...
[...state.brandsFavoritesList, brandData].flat(),
)
: [...state.brandsFavoritesList, {...brandData}],
}));
},
}),
{
name: 'favorites-local',
getStorage: () => zustandStorage,
},
),
);
It looks like you want something like
function updateBrandsFavoritesList(brandsFavoritesList, brandData) {
const brandIndex = brandsFavoritesList.findIndex((brand) => brand.tenant === brandData.tenant);
if (brandIndex < 0) {
// This is a new brand, append it to the list.
return [...brandsFavoritesList, brandData];
}
// This is an existing brand, update it.
const existingBrand = brandsFavoritesList[brandIndex];
const updatedBrand = {
...existingBrand,
products: [...existingBrand.products, ...brandData.products],
};
return brandsFavoritesList.map((brand, index) => (index === brandIndex ? updatedBrand : brand));
}
const updateFavList = (brandData) => {
if (!brandData) return;
set(({brandsFavoritesList}) => ({
brandsFavoritesList: updateBrandsFavoritesList(brandsFavoritesList, brandData),
}));
}
– no need for flat.
The complex logic of finding and updating a brand has been moved to a separate free function which should be easier to e.g. test in isolation too.
(Even better still would be to use TypeScript so errors would be more obvious...)

Redux toolkit filter() not working when wanting to display new array at the same time

I am currently building a clothing shop in which you can add products to the cart, and delete each one of them as you like and it makes the cart re-render and display a new cart without that product within it.
so I've made a Slice for cart in redux. the 'addProduct' part works fine, but the 'deleteProduct' reducer which uses filter() doesn't work. (when i click my delete button nothing happens, and no changes in difference in Redux Devtools)
my slice:
const selectedProductsSlice = createSlice({
name:'selectedProducts',
initialState:{
productList:[],
checkoutPrice:0,
},
reducers: {
addProduct:(state,{payload}) => {
state.productList.push(payload)
state.checkoutPrice += payload.price
},
deleteProduct:(state,{payload}) => {
state.productList.filter((foundProduct) => foundProduct.id !== payload.id)
}
}});
my button and handleDelete:
<button className="btn-delete" onClick={handleDelete(product)}>Delete</button>
function handleDelete(p) {
console.log(p)
dispatch(deleteProduct(p.product))
}
edit:
filter didnt work in every possible way i tried. so i changed my way and did this instead to work. but still i wonder why didnt filter() method work properly.
deleteProduct: (state, { payload }) => {
const foundIndex = state.productList.findIndex((p) => {
return p.product.id === payload.id
})
state.productList.splice(foundIndex,1)
}
The filter operation creates a new array without changing the existing instance. Therefore you need to assign it to state.
deleteProduct: (state, { payload }) => {
return {
...state,
productList: state.productList.filter(
foundProduct => foundProduct.id !== payload.id
)
};
};
You also need to change the way you call the handleDelete in the onClick handler.
onClick={() => handleDelete(product)}
Filter methods returns new array so you have to update your existing array tool. Please update your slice:
const selectedProductsSlice = createSlice({
name:'selectedProducts',
initialState:{
productList:[],
checkoutPrice:0,
},
reducers: {
addProduct:(state,{payload}) => {
state.productList.push(payload)
state.checkoutPrice += payload.price
},
deleteProduct:(state,{payload}) => {
state.productList=state.productList.filter((foundProduct) => foundProduct.id !== payload.id)
}
}});
I went through exactly same problem as you.
I solved this problem just to move my reducer to another existing reducer
Hope you to get through it. I couldn't pass it by

Merging existing localstorage item with state for a shoppingcart

I got a situation where I do not have the experience to know which method is the best and what im doing wrong. The situation is as following:
I got a page with products which have a input + order button, which will add the order to the shoppingcart. My thought was to first set the state for each order you make:
const [amountItem, setAmountItem] = useState({
product: {
id: '',
amount: ''
}
});
Updating:
function handleChange(evt, id) {
const value = evt.currentTarget.value;
setAmountItem({
...amountItem,
product:{
id: id,
amount: value
}
});
console.log(amountItem);
}
Which then I push to the shoppingcart/checkout page (no modal):
if (e.target[0].value < productItem.stock) {
history.push({
pathname: `/winkelwagen/`,
state: {data: amountItem}
});
On this page, i first check if location.state exists before using the shoppingcart component:
if (location.state !== null && shoppingCartItems === '') {
console.log(location.state.data);
setShoppingCartItems(location.state.data);
setShoppingCartActive(true);
let cartString = JSON.stringify(shoppingCartItems);
localStorage.setItem('shopping_carts', cartString)
}
When it does exist, some product is ordered with an amount and must be set to localstorage, the product is 'always' visible when refreshing, etc. Until this point it works, the localstorage item exists:
(key)shopping_carts (value){"product":{"id":3,"amount":"2"}}
After that comes the shoppingcart component:
<ShoppingCart
shoppingCartItems={shoppingCartItems}
setShoppingCartItems={setShoppingCartItems}
shoppingCartActive={shoppingCartActive}
setShoppingCartActive={setShoppingCartActive}
/>
This is where my problem starts. Long story short, it only shows the single item from the state, which obviously will be gone.
In this file I got a useEffect part for the localstorage:
useEffect(() =>{
let shoppingCart = localStorage.getItem("shopping_carts");
console.log('shoppingcartitems ');
shoppingCart = JSON.parse(shoppingCart);
console.log(shoppingCart);
if (shoppingCart !== "") {
const id = shoppingCartItems.id;
const amount = shoppingCartItems.amount;
//setShoppingCartItems(shoppingCart)
setShoppingCartItems(prevState => ({
...prevState,
product: {
...shoppingCartItems,
id: id,
amount: amount
}
}))
}
}, [setShoppingCartItems])
The output for 'shoppingCart' is <empty string>. Why is that? Is the format wrong? I'm also using the localstorage for other info, which works fine. I know the setShoppingCartItems is not correct for multiple values, but I wanted to test this single entry first.
Update:
const CheckoutPage = () => {
const location = useLocation();
const [shoppingCartItems, setShoppingCartItems] = useState('');
const [shoppingCartActive, setShoppingCartActive] = useState(false);
const [mode, setMode] = useState('init');
let savedShoppingCart = JSON.parse(localStorage.getItem("shopping_carts"));
console.log('saved shopping cart: ')
console.log(savedShoppingCart);
if (savedShoppingCart !== "" && mode === 'init') {
const id = savedShoppingCart.id;
const amount = savedShoppingCart.amount;
//setShoppingCartItems(shoppingCart)
setShoppingCartItems(prevState => ({
...prevState,
product: {
...shoppingCartItems,
id: id,
amount: amount
}
}))
setMode('data');
//setShoppingCartActive(true);
}
if (location.state !== null && shoppingCartItems === '') {
console.log(location.state.data);
setShoppingCartItems(location.state.data);
setShoppingCartActive(true);
let cartString = JSON.stringify(shoppingCartItems);
localStorage.setItem('shopping_carts', cartString)
}
return (
<div className="shoppingCartPage">
<ShoppingCart
shoppingCartItems={shoppingCartItems}
setShoppingCartItems={setShoppingCartItems}
shoppingCartActive={shoppingCartActive}
setShoppingCartActive={setShoppingCartActive}
/>
</div>
)
}
So basically I want to do 3 things here:
Get the data from the localstorage item
Is there a saved localstorage item? Add it to existing shoppingCartItems (prevstate)
Save the updated (or new when no localstorage item exists) shoppingCartItems after that
After that I want to pass the data to the shoppingcart where i can increase/decrease items or remove/splice the values.
Treat useEffect with caution as an eventListener on React state.
Therefore you need to specify in the dependency array everything might change, in order to trigger the useEffect callback.
In your useEffect dependencies, where you are updating your shoppingCartItems, you have added only setShoppingCartItems - which I assume that its a setState function. This results in your useEffect te be called only once at the app start because setState functions never change.
So, to have your shoppingCartItems updated via useEffect you need to add it to dependencies.
useEffect(() => {
// your code
}, [setShoppingCartItems, shoppingCartItems])
This may fix your problem, because you never call logic that saves update shopping cart state, the second time, therefore you get empty in your console log.

react redux duplicate keys even when setting a unique key

I have an array of 6 objects which have a uid and nothing else. This is so I can repeat over them and have some placeholder content until an object is ready to be added into the array. I set a unique key when a new object is selected. However if I select the same object twice, even though I'm setting a unique key. It seems to update the unique key on the duplicate item (even though the unique key is different).
Might be easier to see the code/app in action here, an example of the problem would be clicking squirtle then blastoise, take a note of the uid's shown. Then click squirtle again and for some reason it updates the old squirtle with the new squirtles uid causing a duplicate key error. https://codesandbox.io/s/l75m9z1xwq or see code below. Math.random is just placeholder until I can get this working correctly.
const initState = {
party: [
{ uid: 0 },
{ uid: 1 },
{ uid: 2 },
{ uid: 3 },
{ uid: 4 },
{ uid: 5 }
]
};
When I click on something this is triggered:
handleClick = pokemon => {
// setup a uid, will need a better method than math.random later
pokemon.uid = Math.random();
this.props.addToParty(pokemon);
};
This then calls a dispatch which triggers the following reducer. Which essentially just checks if the object has no normal ID then replace the content with the payload sent over. It does this but also somehow updates any previous objects with the same uid even though the if statement does not run against them.
const rootReducer = (state = initState, action) => {
if (action.type === "ADD_POKEMON") {
let foundFirstEmptyPoke = false;
const newArray = state.party.map((pokemon, index) => {
if (typeof pokemon.id === "undefined" && foundFirstEmptyPoke === false) {
foundFirstEmptyPoke = true;
pokemon = action.payload; // set the data to the first object that ios empty
}
// if we get to the last pokemon and it's not empty
if (index === 5 && foundFirstEmptyPoke === false) {
pokemon = action.payload; // replace the last pokemon with the new one
}
return pokemon;
});
return {
party: newArray
};
}
return state;
};
The problem here is that, when you click to select a pokemon, you mutate the data you retrieved from the API:
handleClick = pokemon => {
pokemon.uid = Math.random(); // HERE
this.props.addToParty(pokemon);
};
You actually mutate the react state. What you should do is clone your pokemon data object, add an uid to the clone you just generated and update your redux state with it:
handleClick = pokemon => {
this.props.addToParty({
...pokemon,
uid: Math.random()
});
};
That way, no references to the actual react state are kept. Because that was what was happening when you say it updates the old squirtle with the new squirtles uid. When you tried to add another pokemon, you updated the data you retrieved from your API which was also referenced from your first pokemon slot (from your redux state).
In react/redux it's always better to not mutate objects:
this.props.addToParty({...pokemon, uid: Math.random()});
You are mutating the state. Use spread syntax *** to copy the state before updating.
return {
...state,
party: newArray
}

What is the most comfortable/conventional way to modify a specific item in an array, in a React component state?

I often find my self struggling with manipulating a specific item in an array, in a React component state. For example:
state={
menus:[
{
id:1,
title: 'something',
'subtitle': 'another something',
switchOn: false
},
{
id:2,
title: 'something else',
'subtitle': 'another something else',
switchOn: false
},
]
}
This array is filled with objects, that have various properties. One of those properties is of course a unique ID. This is what i have done recentely to edit a "switchOn" property on an item, according to its ID:
handleSwitchChange = (id) => {
const newMenusArray = this.state.menus.map((menu) => {
if (menu.id === id) {
return {
...menu,
switchOn: !menu.switchOn
};
} else {
return menu;
};
})
this.setState(()=>{
return{
menus: newMenusArray
}
})
}
As you can see, alot of trouble, just to change one value. In AngularJS(1), i would just use the fact that objects are passed by reference, and would directly mutate it, without any ES6 hustle.
Is it possible i'm missing something, and there is a much more straightforward approach for dealing with this? Any example would be greatly appreciated.
A good way is to make yourself a indexed map. Like you might know it from databases, they do not iterate over all entries, but are using indexes. Indexes are just a way of saying ID A points to Object Where ID is A
So what I am doing is, building a indexed map with e.g. a reducer
const map = data.reduce((map, item) => {
map[item.id] = item;
return map;
}, {})
now you can access your item by ID simply by saying
map[myId]
If you want to change it, you can use than object assign, or the ... syntax
return {
// copy all map if you want it to be immutable
...map
// override your object
[id]: {
// copy it first
...map[id],
// override what you want
switchOn: !map[id].switchOn
}
}
As an helper library, I could suggest you use Immutable.js, where you just change the value as it were a reference
I usually use findIndex
handleSwitchChange = (id) => {
var index = this.state.menu.findIndex((item) => {
return item.id === id;
});
if (index === -1) {
return;
}
let newMenu = this.state.menu.slice();
newMenu[index].switchOn = !this.state.menu[index].switchOn;
this.setState({
menu: newMenu
});
}

Categories

Resources