I have 2 states product and variations I call an API and set the values of both state to the API response.
I want the product state to stay as it is and not update
const [product, setProduct] = useState({} as any);
const [variations, setVariations] = useState([] as any);
useEffect(() => {
const getProduct = async () => {
const data = await axios.get("/products?id=4533843820679");
console.log(data);
setProduct(data.data);
// #ts-ignore
setVariations([data.data]);
};
getProduct();
}, []);
In return I map the variations array and return inputs for title, and price and a button to add variations. Adding variations will add another product to variations array. So it just pushes product to variations.
Then I have inputs for title in variation and prices in variation.variants. The problem is with onChange.
When I change the price of one element in variants it changes for all and also changes it for PRODUCT state.
The code can be found here: https://codesandbox.io/s/smoosh-firefly-6n747?file=/src/App.js
Add variations, change prices add another variations and you'll see all issues I'm facing.
It is because of this:
variant.price = e.target.value; // same issue with title
the variant object reference is shared among variations and you are modifying it directly. It is shared because you you made a shallow copy of a variation using ... when adding it.
Here is the solution:
You should update the specific variant object in immutable way (in react you should always update state in immutable way). For that you need to use this as onChange for price:
onChange = {
(e) => {
let updated = variations.map((x) => {
if (x.id === variation.id) {
return {
...x,
variants: x.variants.map((y) => {
if (y.id === variant.id) {
return {
...y,
price: e.target.value
};
}
return y;
})
};
}
return x;
});
setVariations(updated);
}
}
This for onChange for title:
onChange = {
(e) => {
let updated = variations.map((x) => {
if (x.id === variation.id) {
return {
...x,
title: e.target.value
};
}
return x;
});
setVariations(updated);
}
}
NOTE but ids of variations must be different. For testing purposes you can use this as click handler when adding a new variation:
onClick = {
() => {
setVariations((prev) => [...prev, {
...product,
id: Math.floor(Math.random() * 1000) // for testing
}]);
}
}
First, you are not pushing the product to variations. You are overwriting it.
To push a value to array with useState,
setVariations([...variations, product])
But, if you change the product object, variations also gonna be change because it's the same object. (Maybe, react not gonna re-render it but trust me, it is changed.) If you want to keep it same you need to create new object.
So,
setProduct(data.data);
setVariations([...variations, {...data.data}]);
Now, you can change product. variations not gonna change.
This was because you did a shallow copy of an object.
Try to do like this:
setVariations([...variations, data.data,]);
Related
I have a multitabbed view that I am controlling the data with through a global state, being passed through useContext (along with the setState updater function).
The structure is similar to
globalState: {
company: {
list: [
[{name: ..., ...}, {field1: ..., ... }],
[{name: ..., ...}, {field1: ..., ... }],
...
]
},
...
}
I have a table in this first tab, where each row that displays the details in the first object of each inner list array (globalState.company.list[X][0]), and has a few checkboxes to toggle fields in the second object in each inner list array (globalState.company.list[X][1]).
The issue I am having is that when I check a checkbox for a specific field, all companies have that field set to that value before I call setGlobalState(...) in that onChange call from the checkbox itself.
Here is all the related code for the flow of creating the checkbox and the handler:
<td><Checkbox
disabled={tpr.disabled} // true or false
checked={tpr.checked} // true or false
onChange={checkboxOnChange} // function handler
targetId={company.id} // number
field={"field1"} />
</td>
Checkbox definition
const Checkbox = React.memo(({ disabled, checked, onChange, targetId, field }) => {
return (
<input
type="checkbox"
style={ /* omitted */ }
defaultChecked={checked}
disabled={disabled}
onChange={(e) => onChange(e, targetId, field)}
/>
);
});
onChange Handler callback
const checkboxOnChange = (e, id, field) => {
const checked = e.target.checked;
console.log("GLOBAL STATE PRE", globalState.companies.list);
let foundCompany = globalState.companies.list.find(company => company[0].id === id);
foundCompany[1][field].checked = checked;
console.log("foundCompany", foundCompany);
console.log("GLOBAL STATE POST", globalState.companies.list);
setGlobalState(prevState => ({
...prevState,
companies: {
...prevState.companies,
list: prevState.companies.list.map(company => {
console.log("company PRE ANYTHING", company);
if (company[0].id === foundCompany[0].id) {
console.log("Inside here");
return foundCompany;
}
console.log("company", company);
return company;
})
}
}));
};
I see from the GLOBAL STATE PRE log that if I were to check a box for field1, then all companies would have field1 checked well before I modify anything else. I can confirm that before the box is checked, the globalState is as I expect it to be with all of the data and fields correctly set on load.
In the picture below, I checked the box for TPR in the second company array, and before anything else happens, the second and third companies already have the TPR set to true.
Any help would be appreciated, and I will share any more details I am able to share. Thank you.
Just don't mutate the state object directly:
const checkboxOnChange = (e, id, field) => {
const checked = e.target.checked;
setGlobalState(prevState => ({
...prevState,
companies: {
...prevState.companies,
list: prevState.companies.list.map(company => {
if (company[0].id === id) {
return {
...company,
checked
};
}
return {
...company
};
})
}
}));
};
The globalState object is being updated before you call setGlobalState because you are mutating the current state (e.g. foundCompany[1][field].checked = checked;)
One way of getting around this issue is to make a copy of the state object so that it does not refer to the current state. e.g.
var cloneDeep = require('lodash.clonedeep');
...
let clonedGlobalState = cloneDeep(globalState);
let foundCompany = clonedGlobalState.companies.list.find(company => company[0].id === id);
foundCompany[1][field].checked = checked;
I recommend using a deep clone function like Lodash's cloneDeep as using the spread operator to create a copy in your instance will create a shallow copy of the objects within your list array.
Once you have cloned the state you can safely update it to your new desired state (i.e. without worry of mutating the existing globalState object) and then refer to it when calling setGlobalState.
I have state set as follow
const [stories, setStories] = useState([]);
I fetch Data from API in array, and i map the array and set the using setStories as:
setStories(prevState => prevState.concat({user: {name: 'XYZ', profile: 'ABC', stories: [{id: 1, image: 'testing'}];
The above codes are working fine, but i am stuck, when i have to concat the latest story if the id did not matched with fetched data. I have tried below solution but it didnot help:
stories.map(story => {
if(story && story.hasOwnProperty(key)){
//where above key is the user key fetched from the another API, i.e., user key
story?.[key].stories.map(storedStory =>
id(storedStory.id !== fetchedStory.id){
story?.[key].stories.concat({story})}
but the above code did not work, as it only mutate the state and is avoiding re-rendering.
Looking for a clean and efficient method to overcome this. THanks
It's hard to tell what you're trying to accomplish without seeing a full example. But I think your main problem is that you're not using the returned value from map, and from the naming it looks like you're appending the wrong element.
It will help to simplify first.
const newState = stories.map(story => {
if (story?.hasOwnProperty(key)) {
const found = story[key].stories.find(s => s.id === fetchedStory.id);
if (found) {
return story;
} else {
// Let's make a new object with the fetchedStory
// appended into THIS user's stories
return {
...story,
[key]: {
...story[key],
stories: [
...story[key].stories,
// This is supposed to be fetchedStory
// not `story` right??
fetchedStory,
]
}
}
}
} else {
return story;
}
});
setStory(newState);
Edit: You're having a hard time expressing your business logic, and the complexity of the data structure is not helping. So keep simplifying, encapsulate the complex syntax into functions then express your business logic plainly. Ie,
const appendStory = (originalObject, userId, storyToAppend) => {
return {
...originalObject,
[userId]: {
...originalObject[userId],
stories: [
...originalObject[userId].stories,
storyToAppend,
]
}
}
};
const userExistsInList = (users, user) => {
return users?.hasOwnProperty(user);
}
const newStoryAlreadyInStories = (stories, newStory) => {
return stories.find(s => s.id === newStory.id);
}
const newState = stories.map(story => {
if (userExistsInList(story, key)) {
const found = newStoryAlreadyInStories(story[key].stories, fetchedStory);
if (found) {
// User is already in state and the new story is already in the list
// Add business logic here
} else {
// User is already in state and the new story
// is not in their list
// Add business logic here
}
} else {
// User is not in the list yet
// Add business logic here
}
});
I am trying to sort all objects that match the regex into an array.
This does not seem to work with the spread operator and useState, is there any way I can do that?
The result I am getting now is the samples thing only gives me the last object that matches it and nothing else.
The desired effect I want is all the samples that match get pushed into the samples state.
const [accessories, setAccessories] = useState([]);
const [paints, setPaints] = useState([]);
const [samples, setSamples] = useState([]);
// Load order into state
useEffect(() => {
loadUser();
getOrderById(match.params.orderId);
}, []);
// Load order into state
useEffect(() => {
if (!loading) {
console.log(order.line_items);
for (let i = 0; i < order.line_items.length; i++) {
if (order.line_items[i].sku.match(/^(TAC|T.BU.AC)/)) {
console.log('SKU: ', order.line_items[i].sku);
//#ts-ignore
setAccessories([...accessories, order.line_items[i]]);
console.log(accessories);
}
if (order.line_items[i].sku.startsWith('TBA') || order.line_items[i].sku.match(/^TCR(?!0000)/)
|| order.line_items[i].sku.match(/^TCR0000/)) {
//#ts-ignore
setPaints([...paints, order.line_items[i]]);
}
if (order.line_items[i].sku.match(/^TCR\d+P?\d+SAMP/)) {
console.log(samples);
console.log(order.line_items[i]);
//#ts-ignore
setSamples([...samples, ...[order.line_items[i]]]);
}
}
}
}, [loading]);
Well there are few mistakes you're doing here.
Mistake 1:
Calling the same setStates way too many times inside a single useEffect block using a for loop, this might greatly affect React's performance. Again, this is clearly a violation of Rules of Hooks, Only Call Hooks at the Top Level
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions.
Mistake 2:
Though this is not as serious as the previous ones, it's still a mistake. Not using better solutions, Use inbuilt JavaScript methods like filter instead of writing your own for loop
useEffect(() => {
let _accessories;
let _paints;
let _samples;
if (!loading) {
_accessories = order.line_items.filter(({ sku }) => sku.match(/^(TAC|T.BU.AC)/))
_paints = order.line_items.filter(({ sku }) => sku.startsWith('TBA') || sku.match(/^TCR(?!0000)|^TCR0000/))
_samples = order.line_items.filter(({ sku }) => sku.match(/^TCR\d+P?\d+SAMP/))
// Never use setState inside a for loop
// of useEffects
// Also avoid calling same setState multiple times
// use callback setState if you want to access
// previous state, but it ain't a compulsory like
// for class components
setAccessories(s => [ ...s, ..._accessories ])
setPaints(s => [ ...s, ..._paints ])
setSamples(s => [ ...s, ..._samples ])
}
// return in useEffect has different role
// than normal functions
}, [loading])
Spread the results of calling .filter into the calls:
useEffect(() => {
if (loading) {
return;
}
const items = order.line_items;
setAccessories([
...accessories,
items.filter(({ sku }) => sku.match(/^(TAC|T.BU.AC)/))
]);
setPaints([
...paints,
items.filter(({ sku }) => sku.startsWith('TBA') || sku.match(/^TCR(?!0000)|^TCR0000/))
]);
setSamples([
...samples,
items.filter(item => item.sku.match(/^TCR\d+P?\d+SAMP/))
]);
}, [loading]);
I am trying to update an object and its key value inside an array using react redux but as I am new to react and redux, so I am not finding a good way to do this and also the value, is not updating to.
Here is my action
export const addIngredientToMenuItemCartA = (menu_item_id,timestamp,ingrediant,ingrediant_type,selectedMenuItemIngrediantType)
=> async dispatch => {
dispatch({
type: ADD_INGREDIENT_TO_MENU_ITEM_CART,
payload: {
menu_item_id,
timestamp,
ingrediant,
ingrediant_type,
ingrediant_category_type_blue: selectedMenuItemIngrediantType
}
});
};
Here is my reducer
export default function(state=[],action){
case ADD_INGREDIENT_TO_MENU_ITEM_CART:
let menu_item_id = action.payload.menu_item_id;
let ingrediant = action.payload.ingrediant;
let timestamp = action.payload.timestamp;
let items1 = state.slice();
const itemIndexi1 = items1.findIndex(item => item.menu_item_id === menu_item_id);
if(true){
items1[itemIndexi1].ingrediantTotal = ingrediant.price;
}
items1[itemIndexi1].ingrediants.push(ingrediant);
return items1;
default:
return state;
}
I have an array of the cart which has objects inside it and I want to find that specific objects and then update them but if I update them in the reducer then the values are not being changed in the store.
It seems that you are mutating the objects. There is a simple pattern to loop over the list and handle only the relevant item while creating new objects without mutations.
In your case, your code could look something like this:
case ADD_INGREDIENT_TO_MENU_ITEM_CART: {
const { menu_item_id, ingrediant } = action.payload;
const nextState = state.map(item => {
if (item.menu_item_id !== menu_item_id) {
// not our item, return it as is
return item;
}
// this is our relevant item, return a new copy of it with modified fields
return {
...item,
ingrediantTotal: ingrediant.price,
ingrediants: [
...item.ingrediants,
ingrediant
]
}
});
return nextState;
}
Keep in mind, Objects and Arrays are mutable so we can use the spread syntax (...) or .slice etc.
I have a question concerning React and how state must be updated.
Let's say we have a class Players containing in its state an array of objects called players. We want to update one player in this array. I would have done it this way:
class Players extends Component {
state = {
players: []
}
updatePlayer = id => {
const players = this.state.players.map(player => {
player.updated = player.id === id ? true:false;
return player
});
this.setState({players: players});
}
}
But my coworker just did it this way, and it's also working:
updatePlayer = id => {
const playerObj = this.state.players.find(item => {
return item.id === id
})
if (playerObj) {
playerObj.updated = true
this.setState({ playerObj })
}
}
React's function setState update the players array without telling explicitly to do it. So, I have two questions:
Is it using a reference from the find function, and using it to update the players arrays ?
Is one of those ways recommended ?
Thank you all for your explanations !
The difference is that second snippet misuses setState to trigger an update because it uses playerObj dummy property. This could be achieved with forceUpdate.
Neither of these ways are correct. Immutable state is promoted in React as a convention. Mutating existing state may result in incorrect behaviour in components that expect a state to be immutable. They mutate existing player object, and new player.update value will be used everywhere where this object is used, even if this is undesirable.
An idiomatic way to do this is to use immutable objects in state:
updatePlayer = id => {
this.setState(({ players }) => ({
players: players.map(player => ({
...player,
updated: player.id === id
}));
});
}
Notice that setState is asynchronous, updater function has to be used to avoid possible race conditions.
Yes, all it's using a reference. All javascript objects are references so whenever you do a find you get a reference to the object, so mutating it will update it.
const players = this.state.players.map(player => {
return { ...player, updated: player.id === id };
});
this.setState({players: players});
As for the recommended way, you should stick with yours where you explicitly update the state variable that you care about.
Both of them are not correct, because you are mutating state.
The best way is a create a deep copy of this array ( just clone ) and after that make some changes with this cloned array
You can also use lodash _.cloneDeep();
For example
class Example extends React.Component {
state = {
players: [
{id: 0, name: 'John'};
]
};
updatePlayer = id => {
const { players } = this.state;
const clonePlayers = players.map(a => ({...a}));
const player = clonePlayers.find(playerId => playerId === id);
player.name = 'Jack';
this.setState(() => ({
players: clonePlayers
}));
}
render() {
return (
<div>some</div>
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
well, basically they are not the same, your coworkers code is working just because he is using an old reference of the object. so, lets take a look:
updatePlayer = id => {
const players = this.state.players.map(player => {
player.updated = player.id === id ? true:false;
return player
});
this.setState({players: players});
}
on your function, you are creating a new array using your old array, which is the correct way of doing this.
updatePlayer = id => {
const playerObj = this.state.players.find(item => {
return item.id === id
})
if (playerObj) {
playerObj.updated = true
this.setState({ playerObj })
}
}
here your friend is editing the reference of the object that he got using find and then saving a playerObj which is nothing more than the reference of a player from the array that you wanted to edit. after this you should notice that the new state will be something like
this.state = {
players: [p1, p2 ,p3, p4], // each p is a player.
//notice that playerObj is now a reference to some of the P on the players array
playerObj: {
...playerstuff,
updated: true
}
}
hope it helps :)