ddolce
ddolce

Reputation: 815

Best Practice to Implement React Parent-Child Components Using Hooks

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.

Upvotes: 0

Views: 876

Answers (3)

Giorgi Moniava
Giorgi Moniava

Reputation: 28654

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

Upvotes: 1

Tiffany
Tiffany

Reputation: 503

@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>

Upvotes: 0

Dilshan
Dilshan

Reputation: 3001

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

Upvotes: 0

Related Questions