I'm picking up React and not sure if I'm doing this correctly. To preface the question I've read all about the React hooks; I understand them in isolation but am having trouble piecing them together in a real-life scenario.
Imagine I have a Parent component housing a list of Child components generated via a map
function on the parent:
<Parent>
{items.map(i => <Child item={i} />)}
</Parent>
And say the Child component is just a simple:
function Child({item}) {
return <div>{item}</div>
}
However the Child component needs to update its view, and be able to delete itself. My question is - should I call useState(item)
on the child so it internally has a copy of the item? In that case if I updated the item
the items
list in the parent wouldn't get updated right? To fix that I ended up having something that looks like:
<Parent>
{items.map(i =>
<Child
item={i}
updateItem={(index) => setItems( /* slice and concat items list at index */ )}
deleteItem={(index) => setItems( /* slice items list at index */ )}
/>)
}
</Parent>
And the Child
component simply invokes updateItem
and deleteItem
as appropriate, not using any React hooks.
My question here are as follows:
- should I have used
useState
in the child component? - should I have used
useCallback
on theupdateItem
/deleteItem
functions somehow? I tried using it but it didn't behave correctly (the correct item in the Parent got removed but the state in the remaining renderedChild
were showing values from the deletedChild
for example. - My understanding is that this would be very inefficient because an update on 1 child would force all other children to re-render despite them not having been updated.
If done most properly and efficiently, what should the code look like?
Thanks for the pointers.
CodePudding user response:
No you don't have to create internal state. That's an anti pattern to create a local state just to keep a copy of props of the component.
You can keep your state on parent component in your case. Your child component can execute callbacks like you used,
for example,
const [items, _] = useState(initialItemArray);
const updateItem = useCallback((updatedItem) => {
// do update
}, [items])
const deleteItem = useCallback((item) => {
// do delete
}, [items])
<Child
data={item}
onUpdate={updateItem}
onDelete={deleteItem}
/>
Also note you shouldn't over use useCallback
& useMemo
. For example, if your list is too large and you use useMemo for Child items & React re renders multiple 100 - 1000 of list items that can cause performance issue as React now have to do some extra work in memo
hoc to decide if your <Child />
should re render or not. But if the Child component contain some complex UI ( images, videos & other complex UI trees ) then using memo might be a better option.
To fix the issue in your 3rd point, you can add some unique key ids for each of your child components.
<Child
key={item.id} // assuming item.id is unique for each item
data={item}
onUpdate={(updatedItem) => {}}
onDelete={(item) => {}}
/>
Now react is clever enough not to re render whole list just because you update one or delete one. This is one reason why you should not use array index as the key prop
CodePudding user response:
should I have used useState in the child component?
Usually duplicating state is not a good idea; so probably no.
should I have used useCallback on the updateItem/deleteItem functions somehow
You might need it if you want to pass those callbacks to components wrapped in React.memo
.
My understanding is that this would be very inefficient because an update on 1 child would force all other children to re-render despite them not having been updated
Yes your understanding is correct, but whether you would notice the slow down, depends on number of things such as how many child components there are, what each of them renders, etc.
If done most properly and efficiently, what should the code look like?
See below. Notice I added React.memo
which together with useCallback
should prevent those items from re rendering, props of which didn't change.
const Child = React.memo(function MyComponent({ item, update }) {
console.log('Rendered', item);
return (
<div
onClick={() => {
update(item);
}}
>
{item.name}
</div>
);
});
let itemsData = [
{ id: 0, name: 'item1' },
{ id: 1, name: 'item2' },
];
export default function App() {
let [items, setItems] = React.useState(itemsData);
let update = React.useCallback(
(item) =>
setItems((ps) =>
ps.map((x) => (x.id === item.id ? { ...x, name: 'updated' } : x))
),
[]
);
return (
<div>
{items.map((item) => (
<Child key={item.id} item={item} update={update} />
))}
</div>
);
}
Now if you click item1
, console.log
for item2
won't be called.