Ad

How To Avoid Re-rendering When Passing Event Handlers To Nested Functional Components

- 1 answer

Let's say I have a simple functional component that renders many other functional components depending on a filter field. HomeView is the parent functional component, which uses state to handle DomainCard (the child functional component) filtering:

const HomeView = props => {
  // domains
  const domains = useSelector(state => state.domains)
  const [nameFilter, setNameFilter] = useState('')

  const goToDomainDetailView = id => {
    history.push(config.urls.domainLocal.replace(':id', id))
  }

  return (
    <div>
      <div style={{ marginTop: '2rem', marginBottom: '2rem' }}>
        <Input
          icon='search'
          placeholder='Search domain'
          onChange={(e, { value }) => setNameFilter(value)}
        />
      </div>
      <div>
        {domains.data
          .filter(domain => !nameFilter || domain.name.toLowerCase().includes(nameFilter.toLowerCase()))
          .map((domain, idx) => (
            <div key={idx}>
                <DomainCard
                  fluid
                  raised
                  onClick={goToDomainDetailView}
                  id={domain.id}
                  title={domain.name}
                  image={DomainAutomotiveImage}
                />
            </div>
          ))}
      </div>
      </Container>
    </div>
  )
}

Now what happens here, is that while filtering, the components (DomainCard) which are already shown, re-renders, even if memoizing them. That's because the HomeView component itself re-renders due to its state change, and being a functional component, the goToDomainDetailView function is re-defined and changes from previous commit.

So the only thing that came to my mind was to define the goToDomainDetailView outside of the component:

const goToDomainDetailView = id => {
  history.push(config.urls.domainLocal.replace(':id', id))
}

const HomeView = props => {
  // domains
  const domains = useSelector(state => state.domains)
  const [nameFilter, setNameFilter] = useState('')

  return (
    <div>
      <div style={{ marginTop: '2rem', marginBottom: '2rem' }}>
        <Input
          icon='search'
          placeholder='Search domain'
          onChange={(e, { value }) => setNameFilter(value)}
        />
      </div>
      <div>
        {domains.data
          .filter(domain => !nameFilter || domain.name.toLowerCase().includes(nameFilter.toLowerCase()))
          .map((domain, idx) => (
            <div key={idx}>
                <DomainCard
                  fluid
                  raised
                  onClick={goToDomainDetailView}
                  id={domain.id}
                  title={domain.name}
                  image={DomainAutomotiveImage}
                />
            </div>
          ))}
      </div>
      </Container>
    </div>
  )
}

Now, this stuff works, in this case.

But it works because the goToDomainDetailView function does not rely on the component props or on the Redux dispatch function, for example. If that was the case, I simply cannot move the function outside, and the only other way I imagine to avoid re-rendering is to transform the HomeView component in a class Component, so that the goToDomainDetailView function can be defined as a class method, though not changing in case of re-rendering.

So the questions are:

  • Are my assumptions correct?
  • How do you handle this common cases?
  • Which is in your opinion the proper way to use nested functional components with state and avoid re-rendering of all functional child components (for example using the second argument of React.memo and avoiding re-rendering if only the handler function ref changes?).
Ad

Answer

You can use useCallback() hook to memoize callbacks:

...
const somePropDependantCallback = useCallback(
  id => console.log(id, someProp),
  [ someProp ]
);
Ad
source: stackoverflow.com
Ad