Reputation: 195
I just started React, and in this Item list tutorial I have some question about updating the states of the item. Also, I'm using functional component .So in app.js
const [items, setItems] = useState([
{
id: 1,
title: 'Banana',
bought: false
},
...
])
Then I have a function in app.js to update the bought to true or false when I check a box
// The id is passed in from Item.js down below
const markBought = (id) => {
setItems(
items.map(
item => {
if (item.id === id) {
/// If bought is false, checking it will make it true and vice versa
item.bought = !item.bought; // (1)
}
return item; // (2)
})
);
};
return (
<div className="App">
<Items items={items} markBought={markBought}></Items>
</div>
);
The teacher said we are using something called Component Drilling. So in Items.js, we map through every item to display them one by one, but I don't think it is neccessary to show.
Finally in Item.js
<input type="checkbox" onChange={() => props.markBought(props.item.id)} />
{props.item.title}
The application worked perfectly, but it's a little bit confusing for me. So:
Sorry if this is a little bit long, any help will be really appreciated. Thanks for reading
Upvotes: 2
Views: 2776
Reputation: 39250
You are mutating an item in your map, if you optimized your Item component to be a pure component then that component won't re render because of the mutation. Try the following instead:
//use useCallback so marBought doesn't change and cause
// needless DOM re renders
const markBought = useCallback(id => {
setItems((
items //pass callback to the setter from useState
) =>
items.map(
item =>
item.id === id
? { ...item, bought: !item.bought } //copy item with changed value
: item //not this item, just return the item
)
);
}, []);
Here is a full example:
const { useCallback, useState, useRef, memo } = React;
function Items() {
const [items, setItems] = useState([
{
id: 1,
title: 'Banana',
bought: false,
},
{
id: 2,
title: 'Peach',
bought: false,
},
]);
const toggleBought = useCallback(id => {
setItems((
items //pass callback to the setter from useState
) =>
items.map(
item =>
item.id === id
? { ...item, bought: !item.bought } //copy item with changed value
: item //not this item, just return the item
)
);
}, []);
return (
<div>
{items.map(item => (
<Item
key={item.id}
item={item}
toggleBought={toggleBought}
/>
))}
</div>
);
}
//use memo to make Item a pure component
const Item = memo(function Item({ item, toggleBought }) {
const renderedRef = useRef(0);
renderedRef.current++;
return (
<div>
<div>{item.title}</div>
<div>bought: {item.bought ? 'yes' : 'no'}</div>
<button onClick={() => toggleBought(item.id)}>
toggle bought
</button>
<div>Rendered: {renderedRef.current} times</div>
</div>
);
});
//render the application
ReactDOM.render(<Items />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Here is a broken example where you mutate the item and won't see the re render even though the state did change:
const { useCallback, useState, useRef, memo } = React;
function Items() {
const [items, setItems] = useState([
{
id: 1,
title: 'Banana',
bought: false,
},
{
id: 2,
title: 'Peach',
bought: false,
},
]);
const toggleBought = useCallback(id => {
setItems((
items //pass callback to the setter from useState
) =>
items.map(
item =>
item.id === id
? ((item.bought = !item.bought),item) //mutate item
: item //not this item, just return the item
)
);
}, []);
return (
<div>
<div>
{items.map(item => (
<Item
key={item.id}
item={item}
toggleBought={toggleBought}
/>
))}
</div>
<div>{JSON.stringify(items)}</div>
</div>
);
}
//use memo to make Item a pure component
const Item = memo(function Item({ item, toggleBought }) {
const renderedRef = useRef(0);
renderedRef.current++;
return (
<div>
<div>{item.title}</div>
<div>bought: {item.bought ? 'yes' : 'no'}</div>
<button onClick={() => toggleBought(item.id)}>
toggle bought
</button>
<div>Rendered: {renderedRef.current} times</div>
</div>
);
});
//render the application
ReactDOM.render(<Items />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Upvotes: 2
Reputation: 62648
Your map function always returns an item. It just modifies the item first if the item you're modifying matches the id of the item currently being mapped. Map returns a new array of items (even if it doesn't change anything), which causes useState to see a new value. By default, in React, the check for updates isn't very clever - it's just checking if oldValue === newValue
.
For primitives like strings, object equality tests return true for two separate objects, as long as their values match.
"foo" === "foo" // => true
However, this isn't true for object or arrays. Two different arrays containing the same values will not compare as equal (because Javascript isn't comparing their contents, but rather their object IDs):
["foo"] === ["foo"] // => false
So, when you map
your items
, you get a new array object (because recall: map
collects the return values of the callback function into a new array), which will never match the previous value of items
, so every call to setItems
will cause React to say "hm, my previous items
isn't the same object as my new items
, I must re-render this component".
Upvotes: 0
Reputation: 765
Hi there and welcome to Stackoverflow.
You are always returning the item
. You just have an if
statement that will change the bought state and item
will get returned even if the condition above was false, which is the correct way of doing.
Map will indeed not modify the array but return a new one. If you want to get that returning array you could simply do :
const myNewArray = items.map(...)
The way this new array
is getting to your other component is because this new array
is given to your useState()
.
You see setItems()
? This will set your state and Item.js
will automatically be updated.
That is what is so great about react. All components that are served from state
will be updated once this state
is updated.
Upvotes: 0