I am having trouble updating a nested object in an array in react state.
I am trying to compare whether or not an item already exists in the state based off of the name of that item.
Here's what I am trying to have happen:
Add item to cart. If the product name is the same, then increase qty by one. (this works)
If the name is not the same then add that new item to the state. (this also works)
If I go back to a previous item that I have already added, and want to add it again, then I want to find that item in state, compare it to the current object that is being passed in, and update the quantity by one. (this doesn't work)
I decided to implement the immutability-helper to simplify updating state.
I should note, that I am using NextJs and that this problem occurs only after I reload a new dynamic page.
Hope this is enough information... please let me know if more info is needed to be of assistance.
Thanks in advance!
Update 2: This is another problem that I am running into... maybe the answer is obvious, but I thought I'd just put it out there to get a second set of eyes on it (if this should be a new question, please let me know).
I have some optional parameters that go along with each item. The parameters are a nested object inside of the array. My solution was to use the following code to compare the data.
import React, { createContext, useState, useEffect } from 'react';
import update from 'immutability-helper';
export const CartContext = createContext();
export function CartProvider(props) {
const [ cart, setCart ] = useState([]);
//**********Start of the code to check the parameters of each item********
const checkArr = (obj1, obj2) => {
let newArr = [];
function onlyInFirst(first, second) {
for (let i = 0; i < first.length; i++) {
if (second.indexOf(first[i]) === -1) {
newArr.push(first[i]);
}
}
}
onlyInFirst(obj1, obj2);
onlyInFirst(obj2, obj1);
if (newArr.length === 0) {
return false;
} else {
return true;
}
};
//*******End of the code to check the parameters of each item**********
const addItem = (obj) => {
let dataCheck = true;
if (cart.length != 0) {
cart.map((e, i) => {
if (e.productName === obj.productName) {
const prevVal = Object.values(e.productParams);
const currentVal = Object.values(obj.productParams);
dataCheck = checkArr(prevVal, currentVal);
}
if (dataCheck === false) {
const object = e;
const cartCopy = cart;
const newObj = update(object, { quantity: { $set: object.quantity + 1 } });
const newState = update(cartCopy, { [i]: { $set: newObj } });
setCart(newState);
}
});
} else {
setCart([ ...cart, obj ]);
}
if (dataCheck === true) {
setCart([ ...cart, obj ]);
}
};
return <CartContext.Provider value={{ cart, addItem }}>{props.children}</CartContext.Provider>;
}
However, I'm still getting the same sample output as shown below, regardless of what parameters I add to productParams.
Does anyone see anything wrong with my logic here? I am at a loss of what to do...
UPDATE 1: I'm adding the object structure and sample output.
Object structure:
const obj = {
productName: product.name, // product.name comes from api
productParams: {}, // this is dynamically added elsewhere
quantity: 1,
productPrice: price
}
Sample Output From Chrome Dev Tools:
3) [{…}, {…}, {…}]
0: {productName: "Item 1", productParams: {…}, quantity: 4, productPrice: 60}
1: {productName: "Item 2", productParams: {…}, quantity: 3, productPrice: 3000}
2: {productName: "Item 1", productParams: {…}, quantity: 3, productPrice: 60}
length: 3
__proto__: Array(0)
import React, { createContext, useState, useEffect } from 'react';
import update from 'immutability-helper';
export const CartContext = createContext();
export function CartProvider(props) {
const [ cart, setCart ] = useState([]);
const addItem = (obj) => {
if (cart.length != 0) {
cart.map((e, i) => {
if (e.productName === obj.productName) {
const object = e;
const cartCopy = cart;
const newObj = update(object, { quantity: { $set: object.quantity + 1 } });
const newState = update(cartCopy, { [i]: { $set: newObj } });
setCart(newState);
} else {
setCart([ ...cart, obj ]);
}
});
} else {
setCart([ ...cart, obj ]);
}
};
return <CartContext.Provider value={{ cart, addItem }}>{props.children}</CartContext.Provider>;
}
There is a small mistake in your code. You should update cart outside of the map function instead of inside it when there are no matched object in the array.
import React, { createContext, useState, useEffect } from 'react';
import update from 'immutability-helper';
export const CartContext = createContext();
export function CartProvider(props) {
const [ cart, setCart ] = useState([]);
const addItem = (obj) => {
if (cart.length != 0) {
let dataExist = false;
cart.map((e, i) => {
if (e.productName === obj.productName) {
const object = e;
const cartCopy = cart;
const newObj = update(object, { quantity: { $set: object.quantity + 1 } });
const newState = update(cartCopy, { [i]: { $set: newObj } });
setCart(newState);
dataExist=true
}
});
if(dataExist) {
setCart([ ...cart, obj ]);
}
} else {
setCart([ ...cart, obj ]);
}
};
return <CartContext.Provider value={{ cart, addItem }}>{props.children} </CartContext.Provider>;
}
What your code doing was this, if the current item(e) from cart array doesn't match with obj, it was adding that obj in the array. Which should be done only after you have iterate the array and confirmed that there are no data exist in the array which is same as obj.
If that update doesn't solve your problem I might need some sample data(i.e. object structure, sample output etc...) from you to test this properly.
Please update your code with this one and it would be better if you can share obj data and cart data:
const addItem = (obj) => {
if (cart.length !== 0) {
for (let i = 0; i <= cart.length; i += 1) {
if (undefined !== cart[i]) {
if (obj.productName === cart[i].productName) {
const tempArr = [...cart];
tempArr.quantity += 1;
setCart(tempArr);
} else {
setCart([...cart, obj]);
}
}
}
} else {
setCart([...cart, obj]);
}
};
I solved it! InsomniacSabbir had the right idea. I just had to modify the code a bit to get the result I wanted.
Here's the solution
import React, { createContext, useState, useEffect } from 'react';
import update from 'immutability-helper';
export const CartContext = createContext();
export function CartProvider(props) {
const [ cart, setCart ] = useState([]);
const addItem = (obj) => {
let dataCheck = true;
if (cart.length != 0) {
cart.map((e, i) => {
if (e.productName === obj.productName) {
const object = e;
const cartCopy = cart;
const newObj = update(object, { quantity: { $set: object.quantity + 1 } });
const newState = update(cartCopy, { [i]: { $set: newObj } });
setCart(newState);
dataCheck = false;
}
});
} else {
setCart([ ...cart, obj ]);
}
if (dataCheck === true) {
setCart([ ...cart, obj ]);
}
};
return <CartContext.Provider value={{ cart, addItem }}>{props.children}</CartContext.Provider>;
}
I had an if/else statement in map which was causing the problem. I took out the else statement out of map and added another if statement into the function that checks if dataCheck is true/false. dataCheck would be set to false only if the if statement in map was executed.
Hope this answer helps!
Thanks for the help everyone!
Related
I'm trying to store a list of items in localstorage using hooks and state like . but it returns an error
TypeError
cartList is not iterable
this is my code: (and this is the codesandbox)
import { useState, useEffect } from "react";
import { Button } from "react-bootstrap";
export default function App() {
const [cartList, setCartList] = useState([]);
const [cartItem, setCartItem] = useState([]);
useEffect(() => {
let localCart = localStorage.getItem("cartList") || "{}";
console.log("localcart", localCart);
if (localCart) {
localCart = JSON.parse(localCart);
}
setCartList(localCart);
}, []);
const handleClick = (e, item) => {
e.preventDefault();
const arr = e.target.id.split("-");
const selectID = arr[1];
console.log("selectID", selectID);
setCartItem({ ...cartItem, id: selectID });
console.log("cartItem", cartItem);
let itemIndex = -1;
for (const entry of Object.entries(cartList)) {
console.log("entry", entry);
}
if (itemIndex < 0) {
setCartList([...cartList, cartItem]); // error occurs here according to codesandbox
localStorage.setItem("cartList", JSON.stringify(cartList));
}
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Button
variant="link"
id="item-12"
onClick={(e) => handleClick(e, cartItem)}
>
item no.12
</Button>
<Button
variant="link"
id="item-100"
onClick={(e) => handleClick(e, cartItem)}
>
item no.100
</Button>
</div>
);
}
Why you are getting TypeError: cartList is not iterable is because you have initialized the cartList incorrectly.
Please have a try in console following snippet, You will face exactly same error.
console.log( [...{ prop: 'value' }] )
So you should initialize cartList as [] rather than {} inside the useEffect hook like the following.
let localCart = localStorage.getItem("cartList") || "[]";
I think you are confusing about a type of cartList. I guess it would be an Array.
const [cartList, setCartList] = useState([]); // It is declared as Array
useEffect(() => {
let localCart = localStorage.getItem("cartList") || "{}"; // But initialized as Object this line
console.log("localcart", localCart);
if (localCart) {
localCart = JSON.parse(localCart);
}
setCartList(localCart);
}, []);
if (itemIndex < 0) {
setCartList([...cartList, cartItem]); // It should be an Array, but error occured as it's an Obejct.
localStorage.setItem("cartList", JSON.stringify(cartList));
}
As #pilchard pointed out, you are setting cartList as an object on that line:
localStorage.getItem("cartList") || "{}";
So on first render before setting the carlist value to localstorage you are setting the state as an object which cannot be spread inside an array
Here I implemented redux like store using custom hooks. everything goes well and code executed correctly but problem is that in reducer under switch statement "TOGGLE" there I return a updated state which is finally stored in globalstate but if I returned empty object {} instead of {products: updated} still globalstate updating correctly with a change that has been done in reducer...since i am not passing globalstate reference then how it is updated correctly
and what listeners exactly do in dispatch method in code
import MarkFavMulti from "./MarkFavMulti";
import classes from "./MarkFav.module.css";
import useStore from "../HookStore/Store";
import {reducer2} from "../SampleReducer";
const MarkFav = props => {
const [outfit, dispatch] = useStore(reducer2);
const onClicked = (id) => {
dispatch({type: "TOGGLE", id: id});
}
const element = outfit.products.map((item) => {
return <MarkFavMulti cloth={item.name}
favorite={item.favorite}
price={item.price}
key={item.id}
clicked={onClicked.bind(this, item.id)} />
});
return (
<main className={classes.Markfav}>
{element}
</main>
);
};
export default MarkFav;
import {useState, useEffect} from "react";
let globalState = {};
let listeners = [];
const useStore = (reducer) => {
const setState = useState(globalState)[1];
const dispatch = (action) => {
let curr = Object.assign({},globalState);
const newState = reducer({...curr}, action)
globalState = {...globalState,...newState};
for(let listener of listeners) {
listener(globalState);
}
};
useEffect(()=>{
listeners.push(setState);
return () => {
listeners.filter(item => item !==setState);
}
},[setState]);
return [globalState, dispatch];
};
export const initStore = (initialState) => {
if(initialState) {
globalState = {...globalState, ...initialState};
}
}
export default useStore;
let initialState = {
products: [
{ id: 1, name: "shirt", price: "$12", favorite: false },
{ id: 2, name: "jeans", price: "$42", favorite: false },
{ id: 3, name: "coat", price: "$55", favorite: false },
{ id: 4, name: "shoes", price: "$8", favorite: false },
]
}
const configureStore = () => {
initStore(initialState);
};
export default configureStore;
export const reducer2 = (state=initialState, action) => {
switch (action.type) {
case "TOGGLE":
let update = {...state};
let updated = [...update.products];
updated = updated.map(item => {
if(item.id === action.id) {
item.favorite = !item.favorite;
return item;
}
return item;
});
return {products: updated};
//if we return {} ...it will updated correctly in globalstate
default:
throw new Error("not reachable");
}
}
The behavior that you are describing is due to this object assignment right here:
item.favorite = !item.favorite;
Here you are directly mutating the properties of the item object. You probably thought that it would be fine since you are using a copy of the products array.
let update = {...state};
let updated = [...update.products];
What actually happens is that updated is a "shallow copy" of the original array. The array itself is a new array, but the items in that array are the same items as in the state. You can read more about that here.
You need to return a new item object instead of mutating it. Here's a concise way to write it using the ternary operator.
case "TOGGLE":
return {
...state, // not actually necessary since products is the only property
products: state.products.map((item) =>
item.id === action.id
? {
...item,
favorite: !item.favorite
}
: item
)
};
I am experienced js/React developer but came across case that I can't solve and I don't have idea how to fix it.
I have one context provider with many different state, but one state looks like following:
const defaultParams = {
ordering: 'price_asc',
page: 1,
perPage: 15,
attrs: {},
}
const InnerPageContext = createContext()
export const InnerPageContextProvider = ({ children }) => {
const [params, setParams] = useState({ ...defaultParams })
const clearParams = () => {
setParams({...defaultParams})
}
console.log(defaultParams)
return (
<InnerPageContext.Provider
value={{
params: params,
setParam: setParam,
clearParams:clearParams
}}
>
{children}
</InnerPageContext.Provider>
)
}
I have one button on page, which calls clearParams function and it should reset params to default value.
But it does not works
Even when i console.log(defaultParams) on every provider rerendering, it seems that defaultParams variable is also changing when state changes
I don't think it's normal because I have used {...defaultParams} and it should create new variable and then pass it to useState hook.
I have tried:
const [params, setParams] = useState(Object.assign({}, defaultParams))
const clearParams = () => {
setParams(Object.assign({}, defaultParams))
}
const [params, setParams] = useState(defaultParams)
const clearParams = () => {
setParams(defaultParams)
}
const [params, setParams] = useState(defaultParams)
const clearParams = () => {
setParams({
ordering: 'price_asc',
page: 1,
perPage: 15,
attrs: {},
})
}
None of above method works but 3-rd where I hard-coded same object as defaultParams.
The idea is to save dafult params somewhere and when user clears params restore to it.
Do you guys have some idea hot to make that?
Edit:
This is how I update my params:
const setParam = (key, value, type = null) => {
setParams(old => {
if (type) {
old[type][key] = value
} else old[key] = value
console.log('Params', old)
return { ...old }
})
}
please show how you update the "params".
if there is something like this in the code "params.attrs.test = true" then defaultParams will be changed
if old[type] is not a simple type, it stores a reference to the same object in defaultParams. defaultParams.attrs === params.attrs. Since during initialization you destructuring an object but not its nested objects.
the problem is here: old[type][key] = value
solution:
const setParam = (key, value, type = null) => {
setParams(old => {
if (type) {
old[type] = {
...old[type],
key: value,
}
} else old[key] = value
return { ...old }
})
}
I'm using memoize-one on a React component that is basically a table with a rows that can be filtered.
Memoize works great for the filtering but when I want to insert a new row, it won't show up on the table until I either reload the page or use the filter.
If I check the state, the new row's data is in it, so presumably what is happening is that memoize is not allowing the component to re-render even if the state has changed.
Something interesting is that the Delete function works, I am able to delete a row by removing its data from the state and it will re-render to reflect the changes...
Here's the part of the code I consider relevant but if you would like to see more, let me know:
import React, { Component } from "react";
import memoize from "memoize-one";
import moment from "moment";
import {
Alert,
Card,
Accordion,
Button,
Table,
Spinner,
} from "react-bootstrap";
import PropTypes from "prop-types";
import { getRoleMembersDetailed } from "../libs/permissions-manager-client-v1.0";
import RoleMember from "./RoleMember";
import CreateMemberModal from "./CreateMemberModal";
class RoleContainer extends Component {
filter = memoize((roleMembers, searchValue, searchCriterion) => {
const searchBy = searchCriterion || "alias";
return roleMembers.filter((item) => {
if (item[searchBy]) {
if (searchValue === "") {
return true;
}
const value = searchValue.toLowerCase();
if (searchBy !== "timestamp") {
const target = item[searchBy].toLowerCase();
return target.includes(value);
}
// Case for timestamp
const target = moment(Number(item[searchBy]))
.format("MMM DD, YYYY")
.toLowerCase();
return target.includes(value);
}
return false;
});
});
constructor(props) {
super(props);
this.state = {
collapsed: true,
roleAttributes: [],
roleMembers: [],
isLoading: true,
};
}
componentDidMount = async () => {
const roleMembers = Object.values(await this.fetchRoleMembers());
roleMembers.forEach((e) => {
e.alias = e.alias.toLowerCase();
return null;
});
roleMembers.sort((a, b) => {
if (a.alias < b.alias) {
return -1;
}
if (a.alias > b.alias) {
return 1;
}
return 0;
});
// TODO - This logic should be replaced with an API call that describes the roleAttributes.
let roleAttributes = Object.values(roleMembers);
roleAttributes = Object.keys(roleAttributes[0]);
this.setState({
roleMembers,
roleAttributes,
isLoading: false,
});
};
fetchRoleMembers = async () => {
const { roleAttributeName } = this.props;
return getRoleMembersDetailed(roleAttributeName);
};
createRoleMember = (newRoleMembers) => {
const { roleMembers } = this.state;
newRoleMembers.forEach((e) => {
roleMembers.push(e);
});
this.setState(
() => {
roleMembers.sort((a, b) => {
if (a.alias < b.alias) {
return -1;
}
if (a.alias > b.alias) {
return 1;
}
return 0;
});
return { roleMembers };
},
() => {
console.log("sss", this.state);
}
);
};
deleteRoleMember = (alias) => {
this.setState((prevState) => {
const { roleMembers } = prevState;
return {
roleMembers: roleMembers.filter((member) => member.alias !== alias),
};
});
};
render() {
const {
role,
roleAttributeName,
searchValue,
searchCriterion,
userCanEdit,
} = this.props;
const { collapsed, isLoading, roleAttributes, roleMembers } =
this.state;
const filteredRoleMembers = this.filter(
roleMembers,
searchValue,
searchCriterion
);
return (
// continues...
I don't know if it's obvious but there are two functions called filter: this.filter that belongs to memoize and Array.prototype.filter().
I did look around and found these post that says Memoize can be overridden:
If you’ve ran into a UI bug, it is simple to just return false from myComparison to temporarily override the memoization, forcing a refresh on every re-render and returning to the default component behaviour.
But I'm not sure what they mean with "return false from component"
Here's a refactoring of your code to idiomatic React Hooks style (naturally dry-coded).
Note how filtering and sorting the role members is done using useMemo() in a way that doesn't modify state; that's because they can be always recomputed from the stateful data. So long as the useMemo()s' deps array is kept in sync (there're ESLint rules to help with this), this should work with no extra re-renders. :)
Similarly, if you use useCallback (which is a special case of useMemo), you need to keep their deps arrays in sync. If you don't use useCallback, those callbacks may cause re-renders since their identity changes per-render.
import React, { Component } from "react";
import moment from "moment";
import { getRoleMembersDetailed } from "../libs/permissions-manager-client-v1.0";
function filterRoleMembers(
roleMembers,
searchValue,
searchCriterion,
) {
const searchBy = searchCriterion || "alias";
return roleMembers.filter((item) => {
if (item[searchBy]) {
if (searchValue === "") {
return true;
}
const value = searchValue.toLowerCase();
if (searchBy !== "timestamp") {
const target = item[searchBy].toLowerCase();
return target.includes(value);
}
// Case for timestamp
const target = moment(Number(item[searchBy]))
.format("MMM DD, YYYY")
.toLowerCase();
return target.includes(value);
}
return false;
});
}
// TODO: maybe use lodash's `sortBy`?
function compareByAlias(a, b) {
if (a.alias < b.alias) {
return -1;
}
if (a.alias > b.alias) {
return 1;
}
return 0;
}
async function fetchRoleMembers(roleAttributeName) {
return getRoleMembersDetailed(roleAttributeName);
}
async function loadData(roleAttributeName) {
const roleMembers = Object.values(
await fetchRoleMembers(roleAttributeName),
);
roleMembers.forEach((e) => {
e.alias = e.alias.toLowerCase();
});
// TODO - This logic should be replaced with an API call that describes the roleAttributes.
let roleAttributes = Object.values(roleMembers);
roleAttributes = Object.keys(roleAttributes[0]);
return {
roleMembers,
roleAttributes,
};
}
const RoleContainer = ({
role,
roleAttributeName,
searchValue,
searchCriterion,
userCanEdit,
}) => {
const [collapsed, setCollapsed] = React.useState(true);
const [isLoading, setIsLoading] = React.useState(true);
const [roleAttributes, setRoleAttributes] = React.useState([]);
const [roleMembers, setRoleMembers] = React.useState([]);
React.useEffect(() => {
loadData(roleAttributeName).then(
({ roleMembers, roleAttributes }) => {
setRoleAttributes(roleAttributes);
setRoleMembers(roleMembers);
setIsLoading(false);
},
);
}, [roleAttributeName]);
const createRoleMember = React.useCallback(
(newRoleMembers) => {
const updatedRoleMembers = roleMembers.concat(newRoleMembers);
setRoleMembers(updatedRoleMembers);
},
[roleMembers],
);
const deleteRoleMember = React.useCallback(
(alias) => {
const updatedRoleMembers = roleMembers.filter(
(member) => member.alias !== alias,
);
setRoleMembers(updatedRoleMembers);
},
[roleMembers],
);
const filteredRoleMembers = React.useMemo(
() =>
filterRoleMembers(roleMembers, searchValue, searchCriterion),
[roleMembers, searchValue, searchCriterion],
);
const sortedRoleMembers = React.useMemo(
() => [].concat(filteredRoleMembers).sort(compareByAlias),
[filteredRoleMembers],
);
return <>{JSON.stringify(sortedRoleMembers)}</>;
};
I am having problem with my uploadStatus state. I am not getting the updated value of react-hooks state. If I added console.log() inside the fileOnProgress(), I am getting [] value of uploadStatus state.
I tried putting the uploadStatus state in useEffect but infinite loop happens because the state is updating also inside the function.
NOTE: In this scenario the uploadStatus is already populated from other function, that's why I am expecting to get the updated value.
import React, { useEffect, useState } from 'react';
function Dropzone {
const [ uploadStatus, setUploadStatus ] = useState([]);
const [ resumableFiles, setResumableFiles ] = useState([]);
const resumableListener = () => {
if (resumableFiles.length === 0) return;
resumableFiles.map(resumable => {
resumable.on('progress', () => {
fileOnProgress(resumable);
});
resumable.on('fileError', (error) => {
console.log(error)
});
});
};
const fileOnProgress = (resumable) => {
const file = resumable.files[0];
const size = (file.size / 1048576).toFixed(2);
const progress = (resumable.progress() * 100).toFixed(2).toString() + '%';
const cont = [...uploadStatus];
cont.map(d => {
if (d.id === file.uniqueIdentifier) {
d.status = progress;
}
});
setUploadStatus(cont);
};
useEffect(() => {
resumableListener();
}, [resumableFiles]);
...
}
Array.map returns a new array, so try :
const newCont = cont.map(d => {
if (d.id === file.uniqueIdentifier) {
d.status = progress;
}
return d;
});
setUploadStatus(newCont);
As pointed out by #go_diego , you are also missing the return in the map.
MDN docs for reference