0

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:

  1. should I have used useState in the child component?
  2. should I have used useCallback on the updateItem/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 rendered Child were showing values from the deleted Child for example.
  3. 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.

ddolce
  • 739
  • 2
  • 10
  • 30

3 Answers3

1

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 - which means item2 didn't rerender

Giorgi Moniava
  • 27,046
  • 9
  • 53
  • 90
  • Thanks for the answer- is the reason why we have React.Memo on the child component because we can’t use hooks (specifically useMemo) in a for loop? – ddolce Dec 23 '22 at 20:10
  • @ddolce No `react.memo` and `useMemo` have different usages – Giorgi Moniava Dec 23 '22 at 20:31
  • Hmm.. correct me I’m probably wrong but If we aren’t looking at a list of Child components (say we’re rendering only one,) would `React.memo(function Child(…))` not be the same as having `const c = useMemo(, [])` in the parent component? – ddolce Dec 23 '22 at 23:11
  • @ddolce Sometimes you can use `useMemo` (but it takes function as argument) to achieve same functionality as `React.memo`, but I said in general they have different purposes, one is for memoizing result of computation other is for components. – Giorgi Moniava Dec 24 '22 at 10:43
0

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

Dilshan
  • 2,797
  • 1
  • 8
  • 26
  • Ah so react uses id to determine whether to re-render a child? Think that explains why I saw the deleted item being rendered still- I was using the index of the items as the keys. Say I have 2 elements so indexes 0, 1. If I deleted element 0 though element 1 is left the data from element 0 would still be rendered in lieu of element 1 – ddolce Dec 23 '22 at 20:16
  • @ddolce Using correct keys (such as id) is important but it *doesn't stop* from rerendering components props of which didn't change. In my example, try remove `React.memo`, and when you click `item1`, see the `console.log` for `item2` will also be printed - which means `item2` was still rerendered. – Giorgi Moniava Dec 23 '22 at 21:21
  • [Check this](https://overreacted.io/react-as-a-ui-runtime/#lists). Also as @GiorgiMoniava mentioned you can use `memo` to prevent unnecessary re render that happens due to the updating state. This is because you have to make at least a shallow copy of your item array before modify in onUpdate handler. Since the copy is different from original data each react component will re re render but that doesn't mean it will recreate or update the corresponding DOM node ( i.e it won't commit to the DOM ) – Dilshan Dec 24 '22 at 01:31
  • As I mentioned in my answer, it is up to you to decided whether to use the memo or not use the memo. [check this](https://stackoverflow.com/a/63405621/11306028) – Dilshan Dec 24 '22 at 01:34
0

@Giorgi Moniava's answer is really good. I think you could do without useCallback as well and still adhere to best practices.

const {useEffect, useState} = React;

const Child = ({ item, update }) => {
  const [rerender, setRerender] = useState(0);
  useEffect(() => setRerender(rerender + 1), [item]);
  useEffect(() => setRerender(rerender + 1), []);

  return (
    <div className="row">
      <div className="col">{item.id}</div>
      <div className="col">{item.name}</div>
      <div className="col">{item.value}</div>
      <div className="col">{rerender}</div>
      <div className="col">
        <button onClick={update}>Update</button>
      </div>
    </div>
  );
}

const Parent = () => {
  const [items, setItems] = useState([
    {
      id: 1,
      name: "Item 1",
      value: "F17XKWgT"
    },
    {
      id: 2,
      name: "Item 2",
      value: "EF82t5Gh"
    }
  ]);

  const add = () => {
    let lastItem = items[items.length - 1];
    setItems([
      ...items,
      {
        id: lastItem.id + 1,
        name: "Item " + (lastItem.id + 1),
        value: makeid(8)
      }
    ]);
  };

  const update = (sentItem) => {
    setItems(
      items.map((item) => {
        if (item.id === sentItem.id) {
          return {
            ...item,
            value: makeid(8)
          };
        }
        return item;
      })
    );
  };

  const makeid = (length) => {
    var result = "";
    var characters =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    var charactersLength = characters.length;
    for (var i = 0; i < length; i++) {
      result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
  };

  return (
    <div className="parent">
      <div className="header">
        <h1>Parent Component</h1>
        <h2>Items in Parent State</h2>
      </div>
      <div className="table">
        <section>
          <header>
            <div className="col">ID</div>
            <div className="col">NAME</div>
            <div className="col">VALUE</div>
          </header>

          {items.map((item, i) => (
            <div className="row" key={item + "-" + i}>
              <div className="col">{item.id}</div>
              <div className="col">{item.name}</div>
              <div className="col">{item.value}</div>
            </div>
          ))}
        </section>

        <div className="header">
          <h1>Children</h1>
          <h2>Based on Items state</h2>
        </div>
        <button onClick={add}>Add</button>
        <section>
          <header>
            <div className="col">ID</div>
            <div className="col">Name</div>
            <div className="col">Value</div>
            <div className="col">Re-render</div>
            <div className="col">Update</div>
          </header>

          {items.map((item, i) => (
            <Child
              item={item}
              key={"child-" + item + "-" + i}
              update={() => update(item)}
            />
          ))}
        </section>
      </div>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById("root")
);
.parent {
  font-family: sans-serif;
}
.header {
  text-align: center;
}

section {
  display: table;
  width: 100%;
}

section > * {
  display: table-row;
  background-color: #eaeaea;
}

section .col {
  display: table-cell;
  border: 1px solid #cccccc;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
Tiffany
  • 487
  • 2
  • 13