So, I'm just going through a React course. Learning this framework for the first time so this is likely a dumb question. Here's code I've been given for updating a property on an object stored in state as an array of objects.
const [squares, setSquares] = React.useState(boxes)
function toggle(clickedSquare) {
setSquares(prevSquares => {
return prevSquares.map((square) => {
return square === clickedSquare ? {...square, on: !square.on} : square
})
})
}
...but, the following code I wrote works too and seems simpler, what's wrong with this approach? State values are only shallow immutable. Objects stored in a state array are themselves mutable, as far as I can tell...
const [squares, setSquares] = React.useState(boxes)
function toggle(clickedSquare) {
clickedSquare.on = !clickedSquare.on;
setSquares(prevSquares => [...prevSquares])
}
Also consider this example where I have an array containing deeply nested objects held in state.
[
{
trunk: {
limb: {
branch: {
twig: {
leaf: {
color: "green"
}
}
}
}
}
}
]
I want to change that "green" to "brown". It states in this article Handling State in React that...
Deep cloning is expensive
Deep cloning is typically wasteful (instead, only clone what has actually changed)
Deep cloning causes unnecessary renders since React thinks everything has changed when in fact perhaps only a specific child object has changed.
The thing that has changed in the tree example is just the leaf object. So only that needs to be cloned, not the array and not the whole tree or trunk object. This makes a lot more sense to me. Does anyone disagree?
This still leaves (no pun intended) the question of what bugs can be introduced by updating property values in a state array my way and not cloning the object that has the change? A single concrete example would be very nice just so I can understand better where I can optimize for performance.
clickedSquare.on = !clickedSquare.on; is a state mutation. Don't mutate React state.
The reason the following code is likely working is because it has shallow copied the squares state array which triggers a rerender and exposes the mutated array elements.
function toggle(clickedSquare) {
clickedSquare.on = !clickedSquare.on; // <-- mutation!
setSquares(prevSquares => [...prevSquares]); // new array for Reconciliation
}
It may not have any adverse effects in this specific scenario, but mutating state is a good foot gun and likely to cause potentially difficult bugs to debug/diagnose, especially if their effects aren't seen until several children deeper in the ReactTree.
Just always use the first method and apply the Immutable Update pattern. When updating any part of React state, even nested state, new array and object references need to be created for React's Reconciliation process to work correctly.
function toggle(clickedSquare) {
setSquares(prevSquares => prevSquares.map((square) => // <-- new array
square === clickedSquare
? { ...square, on: !square.on } // <-- new object
: square
));
}
Here, You are using map() method which return only true or false for that condition.
So, You should use filter() method instead of map() method which return filtered data for that condition.
For Example:
const arr = [
{
name: 'yes',
age: 45
},
{
nmae: 'no',
age: 15
}
]
const filterByMap = arr.map(elm => elm.age > 18)
console.log(filterByMap) // outputs --> [ true, false ]
const filterByFilter = arr.filter(elm => elm.age > 18)
console.log(filterByFilter) // outputs --> [ { name: 'yes', age: 45 } ]
Related
I want to use React.useReducer to update state. My state is an array of objects. When update action is triggered, not only value from desired index is updated but all of them. I want to have updated only the value from indicated array index. How can I do that?
After I click button1, I want to get
[{"amount":null,"kcal":null,"name":null,"isPieceType":false},
{"amount":null,"kcal":null,"name":null,"isPieceType":false},
{"amount":null,"kcal":125,"name":null,"isPieceType":false},
{"amount":null,"kcal":null,"name":null,"isPieceType":false}]
instead of
[{"amount":null,"kcal":125,"name":null,"isPieceType":false},
{"amount":null,"kcal":125,"name":null,"isPieceType":false},
{"amount":null,"kcal":125,"name":null,"isPieceType":false},
{"amount":null,"kcal":125,"name":null,"isPieceType":false}]
I tried to copy state as const newState = [...state] and use lodash's cloneDeep. Below, link to jsfiddle with code to reproduce.
https://jsfiddle.net/wtj5eyfh/
Your initial state of ingredientsState has references to the same object called initialIngredient. That caused everything to update when one entry was updated. Even though
const stateToUpdate = [...state]; created a new array again all entries refers to the same object.
Fix
Change the following referenced array entries
const [ingredientsState, ingredientsDispatch] = React.useReducer(mealReducer, [
initialIngredient,
initialIngredient,
initialIngredient,
initialIngredient
]);
To be an array of copies of initialIngredient object (spread operator simply create clones of the referred object)
const [ingredientsState, ingredientsDispatch] = React.useReducer(mealReducer, [
{ ...initialIngredient },
{ ...initialIngredient },
{ ...initialIngredient },
{ ...initialIngredient }
]);
JS Fiddle
I've been working on a SPA for a while and managing my global state with a custom context API, but it's been causing headaches with undesired rerenders down the tree so I thought I'd give react-easy-state a try. So far it's been great, but I'm starting to run into some issues which I assume has to do with the mutability of the global state, something which was easily solved with the custom context api implementation using a lib like immer.
Here's a simplified version of the issue I'm running into: I have a global state for managing orders. The order object primaryOrder has an array of addons into which additional items are added to the order - the list of available addons is stored in a separate store that is responsible for fetching the list from my API. The orderStore looks something like this:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
}
})
When a user selects to increases the quantity of an addon item, it's added to the addons array if it isn't already present, and if it is the qty prop of the addon is increased. The same logic applies when the quantity is reduced, except if it reaches 0 then the addon is removed from the array. This is done using the following methods on the orderStore:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
},
get orderAddons() {
return orderStore.primaryOrder.addons;
},
increaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
let updatedItem = {
...item,
qty: 1,
};
orderStore.primaryOrder.addons = [
...orderStore.primaryOrder.addons,
updatedItem,
];
} else {
orderStore.primaryOrder.addons[index].qty += 1;
}
console.log(orderStore.primaryOrder.addons);
},
decreaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
return;
} else {
// remove the item from the array if value goes 1->0
if (orderStore.primaryOrder.addons[index].qty === 1) {
console.log("removing item from array");
orderStore.primaryOrder.addons = _remove(
orderStore.primaryOrder.addons,
(i) => i.id !== item.id
);
console.log(orderStore.primaryOrder.addons);
return;
}
orderStore.primaryOrder.addons[index].qty -= 1;
}
}
})
The issue I'm running into has to do with the fact that one of my views consuming the orderStore.addons. My Product component is the consumer in this case:
const Product = (item) => {
const [qty, setQty] = useState(0);
const { id, label, thumbnailUrl, unitCost } = item;
autoEffect(() => {
if (orderStore.orderAddons.length === 0) {
setQty(0);
return;
}
console.log({ addons: orderStore.orderAddons });
let index = orderStore.orderAddons.findIndex((addon) => addon.id === id);
console.log({ index });
if (index !== -1) setQty(orderStore.findAddon(index).qty);
});
const Adder = () => {
return (
<div
className="flex"
style={{ flexDirection: "row", justifyContent: "space-between" }}
>
<div onClick={() => orderStore.decreaseAddonItemQty(item)}>-</div>
<div>{qty}</div>
<div onClick={() => orderStore.increaseAddonItemQty(item)}>+</div>
</div>
);
}
return (
<div>
<div>{label} {unitCost}</div>
<Adder />
</div>
)
}
export default view(Product)
The issue occurs when I call decreaseAddonItemQty and the item is removed from the addons array. The error is thrown in the Product component, stating that Uncaught TypeError: Cannot read property 'id' of undefined due to the fact that the array length reads as 2, despite the fact that the item has been removed ( see image below)
My assumption is that the consumer Product is reading the global store before it's completed updating, though of course I could be wrong.
What is the correct approach to use with react-easy-state to avoid this problem?
Seems like you found an auto batching bug. Just wrap your erroneous mutating code in batch until it is fixed to make it work correctly.
import { batch, store } from '#risingstack/react-easy-state'
const orderStore = store({
decreaseAddonItemQty(item) {
batch(() => {
// put your code here ...
})
}
})
Read the "Reactive renders are batched. Multiple synchronous store mutations won't result in multiple re-renders of the same component." section of the repo readme for more info about batching.
And some insight:
React updates are synchronous (as opposed to Angular and Vue) and Easy State (and all other state managers) use React setState behind the scenes to trigger re-renders. This means they are all synchronous too.
setState usually applies a big update at once while Easy State calls a dummy setState whenever you mutate a store property. This means Easy State would unnecessarily re-render way too often. To prevent this we have a batch method that blocks re-rendering until the whole contained code block is executed. This batch is automatically applied to most task sources so you don't have to worry about it, but if you call some mutating code from some exotic task source it won't be batched automatically.
We don't speak about batch a lot because it will (finally) become obsolete once Concurrent React is released. In the meantime, we are adding auto batching to as many places as possible. In the next update (in a few days) store methods will get auto batching, which will solve your issue.
You may wonder how could the absence of batching mess things up so badly. Older transparent reactivity systems (like MobX 4) would simply render the component a few times unnecessarily but they would work fine. This is because they use getters and setters to intercept get and set operations. Easy State (and MobX 5) however use Proxies which 'see a lot more'. In your case part of your browser's array.splice implementation is implemented in JS and Proxies intercept get/set operations inside array.splice. Probably array.splice is doing an array[2] = undefined before running array.length = 2 (this is just pseudo code of course). Without batching this results in exactly what you see.
I hope this helps and solves your issue until it is fixed (:
Edit: in the short term we plan to add a strict mode which will throw when store data is mutated outside store methods. This - combined with auto store method batching - will be the most complete solution to this issue until Concurrent React arrives.
Edit2: I would love to know why this was not properly batched by the auto-batch logic to cover this case with some tests. Is you repo public by any chance?
This is more of a brainstorming question as I can't really seem to come up with a good solution.
I have a component which renders a tree based on some passed JSON (stored at the top level). Each node of the tree can have 0..n children and maps to a component defined by the JSON of that node (can be basically anything is the idea). The following is just an example and the names don't mean anything specific. Don't pay too much attention to the names and why a UserList might have children that could be anything.
JSON: {
data: {}
children: [
{
data: {}
children: []
},
{
data: {}
children: []
},
{
data: {}
children: [
{
data: {}
children: []
},
...etc
]
},
]
}
const findComponent = (props) => {
if (props.data.name === "userSelector") {
return <UserSelectorNode {...props}>;
} else if (props.data.name === "userInformation") {
return <UserInformationNode{...props}>; // example of what might be under a userSelectorNode
}
...etc
};
// render a user selector and children
const UserSelectorNode = (props) => {
const [selected, setSelected] = React.useState([])
// other methods which can update the JSON when selected changes...
return (
<div>
<UserSelector selected={selected}/> // does a getUser() server request internally
<div>
{props.data.children.map((child) => findComponent(child))}
<div>
</div>
);
};
This tree can be modified at any level (add/remove/edit). Adding/Editing is easy. The problem is remove operations.
Some children components use existing components which do things like getting a list of users and displaying them in a list (stored in state I have no access to). When a node on the tree is removed new components are made for every node that has to shift (JSON at the index is now different), which can be a lot. This causes a bunch of requests to occur again and sometimes state can be lost entirely (say the page number of a table to view users).
I'm pretty sure there is no way for the "new" UserSelector created when the JSON shifts to keep the same state, but I figured I may as well ask if anyone has dealt with anything similar and how they went about designing it.
The only way I can think of is to not actually reuse any components and re implement them with state stored somewhere else (which would suck), or rewrite everything to be able to take internal state as well as an external state storage if required.
EDIT: Added Sandbox
https://codesandbox.io/s/focused-thunder-neyxf. Threw it together pretty quick to only get a single layer of remove working which shows the problem.
Let's say, I have a state that looks as follows.
constructor(props) {
super(props);
this.state = {
setting_a: "value-1",
setting_b: "color-green"
}
}
When I change the state of a specific setting (e.g. setting_a), I don't want other settings (e.g. setting_b) to disappear. So I also specify the other settings while changing the state. (which is easy using the spread operator ...state).
this.setState( {...this.state, setting_a: "value-2"});
I noticed though, that some tutorials restate them, and others only specify the changed key-values.
Things got just a little bit more complicated since the introduction of the Component#getDerivedStateFromProps method, ( since React 16.3 ).
static getDerivedStateFromProps(props, state) {
const oldSetting = state.setting_a;
const newSetting = props.setting_a;
if (oldSetting !== newSetting) {
// this is a very similar situation.
return ({ ...state, state.setting_a: props.setting_a});
}
return null;
}
Again, in the above example, I add all previous settings (i.e. ...state), because I don't want the other settings to be removed.
In both these cases, the same question: do I need to specifically repeat values which are already in the state ? Or are the states always merged incrementally, without removing ?
You don't need to copy the state (using spread operator or any idea) when updating the state with setState. The setState method updates the required state only:
this.setState( {setting_a: "value-2"});
So, now you will still get:
state = {
setting_a: "value-2",
setting_b: "color-green"
}
Similarly, it works like that when you return the object in getDerivedStateFromProps. The returned value is applied in the state without mutation.
You only need to copy the state when you want to update the property of state. For eg.:
// initial state
this.state = {
settings: {
a: 'value-1',
b: 'color-green'
}
}
Now, we have a and b property in settings state. So now, if you wanted to update the a, then you'll need to copy the settings:
this.setState((state) => ({settings: {...state.settings, a: 'value-2' } }))
The preceding example is with settings object. You can think similar with array of state. You may just do a google search for how to update the array without mutation?
It depends.
In your first case you could do:
this.setState( prevState => {
prevState.setting_a = "value-2";
return prevState
});
Or just go with:
this.setState({ setting_a: "value-2" });
As per React Docs State Updates are Merged.
this is a piece of code I came across and I am not sure what it is doing actually?
it feels to me like it is mutating the state
Version 1:
this.state.items = [...this.state.initialItems];
Now, is the above code mutating state??
And can the above also be written as Version 2:
this.setState({
items:[...this.state.initialItems];
});
the thing is that version 1 is working but version 2 isnt.
So,what's wrong here?
what am I missing?
what does ...this.state.initialItems do? is that the spread operator?
This code is definitely mutating state. Don't do it:
this.state.items = [...this.state.initialItems];
Yes, the ... is a spread operator in this context.
For version 2, make sure you don't use the semicolon.
It should just be :
this.setState({
items:[...this.state.initialItems]
});
And of course like the poster below has said, it's asynchronous so you won't see the results right away.
Augmentation 1:
If you want to do things after your state is ready, you can add a callback as your 2nd argument to setState.
this.setState({
items:[...this.state.initialItems]
}, ()=>{
// do stuff
}
);
Augmentation 2:
Your first argument to setState can be a function as well, as the other poster has mentioned, so that when you access state it will be the "freshest" copy, and not the one that was captured when you created the object that you passed to setstate:
this.setState((stateNOW) => {
return {items:[...stateNOW.initialItems]};
}, ()=>{
// do stuff
}
);
Version 1 is wrong. But the real question is Why?
state is a special object in React and it is not to be mutated.
Say you declare an object:
const obj = {
label: 'doe'
value: 'foo'
}
Then, you update the obj with
obj.value = 'bar'
This is the process of mutation and should never perform it with a state variable. Instead we use setState as a request to change the state.
... is the spread syntax.
As the other answers have mentioned, the reason why you think it is not working might be because state updates are asynchronous.
You have to use the setState callback or componentDidUpdate lifecycle hook to log the value of state.
setState(stateChange[, callback])
Another issue in the code is the use of this.state.something inside setState. Like we already mentioned before setState is asynchronous and a number of state updates may be merged. So using this.state.initialItems may not always work out for you, if you miss a update.
this.setState((prevState) => {
return {items: [...prevState.initialItems]};
});
This would be the right way to use a value of previous state.
Version 1 is mutating your state and this is a bad practice.
Version 2 is not and the reason why it's not working (actually it is) is that calls to setState are asynchronous and therefore you will not have access to its up-to-date value in the next line.
Should you need access to the new state value, you can use:
this.setState(prevState => ({
items: [...prevState.initialItems],
}), () => {
console.log(this.state) // new state value
})
Please also refer to this link in ReactJS docs about how to set state properly.
The ... is indeed the spread syntax.
You can use it, for example:
To add items to an array
const arr = [1, 2, 3, 4]
const newArr = [...arr, 5, 6] // This will spread everything in arr and add numbers 5 and 6 to newArr
console.log('arr', arr)
console.log('newArr', newArr)
To get items from an array
const arr = [1,2,3,4]
const [firstItem, secondItem, ...others] = arr
console.log('firstItem:', firstItem)
console.log('secondItem:', secondItem)
console.log('others:', others)
To create a new object
const obj = {
name: 'Stack Overflow',
age: 10,
}
const newObj = {
...obj, // gets all properties from obj
age: 12, // but only change age
}
console.log('obj', obj)
console.log('newObj', newObj)