DEV Community

zelofff
zelofff

Posted on

Redundant re-renders in React

Table of Contents


Before you optimize the amount of re-renders in React application, try to profile and find if there is a problem.

If you profiled and see the problem with heavy re-renders which affect performance, then you might try to optimize it.

Next, we'll see in which cases React components re-render and how to avoid it.

When components re-render?

Look at the example below

const Child = () => <p>some text</p>

const Parent = () => { 
  const [count, setCount] = useState(0)

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>
        {count}
      </button>
      <Child />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Will Child re-render on the button click? Spoiler: yes

React re-renders all the component's tree by default if the state or props changes. That's why in the example above Child will re-render.

The thing is that Child's re-render is not a problem as long as the render is lightweight. Often it is better for performance to re-render, than use optimizations like useMemo, useCallback, React.memo, etc.

useCallback and useMemo

Don't use these hooks by default. It is crucial to get used to the fact that it is a bad practice to put useCallback and useMemo hooks everywhere.

useCallback and useMemo are just functions with their own abstractions. On every render, these functions are called, and a callback that is passed as an argument is created again.

However, if child components are not wrapped in React.memo, there will be no effect and everything will re-render.

You can use useCallback or useMemo in these cases:

  • Child components are wrapped in memo and you don't want them to re-render
  • You have heavy calculations that you don't want to recalculate on each render

React.memo

This hoc is useful when you have a deep component tree or some heavy calculations on render

const Component = (props) => {
  return (
    // deep tree of components 
  )
}

export const MemoizedComponent = React.memo(Component)
Enter fullscreen mode Exit fullscreen mode

Component wrapped in memo would re-render only if props, internal state, or context will change.

It is important to remember that React.memo uses shallow equal of props by default. That's why you should memoize non-primitive values like objects, arrays, functions, etc. in the parent component.

Move state down

Let's imagine you have this component:

const Parent = () => {
  const [count, setCount] = useState(0)

  return (
    <>
      <ComponentA onChange={setCount} count={count} />
      <ComponentB count={count} />
      <ComponentC />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Component A and component B are using count state, but component C doesn't need it. If we let it be as it is, component C will re-render each time the count changes if it is not wrapped in React.memo.

We can move count state down by creating another component like this:

const Union = () => {
  const [count, setCount] = useState(0)

  return (
    <>
      <ComponentA onChange={setCount} count={count} />
      <ComponentB count={count} />
    </>
  )
} 

const Parent = () => {
  return (
    <>
      <Union />
      <ComponentC />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now component C does not depend on count state and will not re-render when the count changes.

Move content up

Let's complicate the previous example

const Parent = () => {
  const [count, setCount] = useState(0)

  return (
    <ComponentA onChange={setCount} count={count}>
      <ComponentB count={count} />
      <ComponentC />
    </ComponentA>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now component A wraps all the content and we can't move the state down. In that case, we can move component A and component B up.

const Union = ({ children }) => {
  const [count, setCount] = useState(0)

  return (
    <ComponentA onChange={setCount} count={count}>
      <ComponentB count={count} />
      {children}
    </ComponentA>
  )
}
Enter fullscreen mode Exit fullscreen mode

After we moved component A, component B, and their state up, we can pass component C as a child in Union component.

const Parent = () => {
  return (
    <Union>
      <ComponentC />
    </Union>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now again, if count state changes, component C will not re-render.

HOC for loading

Sometimes we want to handle some loading state inside a component.

For example:

const Parent = ({ isLoading }) => {
  const someCb = useCallback(...)
  const someCalculatedValue = useMemo(...)

  useEffect(...)

  if (isLoading) {
    return <Loader />
  }

  return ...
}
Enter fullscreen mode Exit fullscreen mode

Here we see a lot of hooks, that we don't need while data is loading. It is redundant to create a callback, calculate values, or fire effects while isLoading is false.

If you try to move isLoading condition upwards, when isLoading changes from true to false, the number of hooks on render will change, and React will fire an error.

To avoid errors and redundant hooks we can move this condition in HOC.

const WithLoading = (Component) => {
  return ({ isLoading, ...props }) => {
    if (isLoading) {
      return <Loader />
    }

    return <Component {...props} />
  }
}
Enter fullscreen mode Exit fullscreen mode

Then we can use WithLoading HOC like this.

const Parent = () => { ... }

export const WithLoadingParent = WithLoading(Parent)
Enter fullscreen mode Exit fullscreen mode

Uncontrolled input

Quite often you can see controlled input in React apps. Many developers use this pattern by default even if there is no reason to use it.

If you don't need to validate input value or run any other logic on the fly, you can just use native inputs.

const Parent = () => {
  const inputRef = useRef()

  const handleSearch = () => {
    console.log(inputRef.current.value)
  }

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleSearch}>Search</button>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Thereby component will not re-render when the input's value changes, but we can easily get the value when we need it.

Wrap up

The methods described above are only applicable if there are problems with heavy re-renders that you want to avoid.

It is very important to remember that optimizations are often not free. You should profile first and only then try to solve the problems.

Top comments (5)

Collapse
 
brense profile image
Rense Bakker

Strong disagree. useCallback is always a good idea and should always be used. Not using useCallback is just a shortcut that people take, because they don't want to bother providing a dependency array, because they do not fully understand how dependency arrays work and thats the #1 reason why React apps break down. Anything that you provide as props to a component, should always be memoized in some way. If you don't memoize the props that you provide to a component, it's going to ALWAYS rerender, You basically give up any control over how/when a component rerenders. It may be easy to understand why a component is rerendering if your app is only 1 level deep. But when there's a tree of 50 components that all rerender because at the top level, you have one unmemiozed prop, it becomes an absolute nightmare to debug. Props should only change when you actually intend for them to change.

function Component(props){
  function myUnmemoizedCallback(){
    // do something
  }
  return <SomeOtherComponent callback={myUnmemoizedCallback} />
  // SomeOtherComponent is going to rerender everytime when
  // the props of Component change, because of the unmemoized callback
}
Enter fullscreen mode Exit fullscreen mode
function Component(props){
  const myMemoizedCallback = useCallback(() => {
    // do something that depends on props.onlyPropNeeded
  }, [props.onlyPropNeeded]) // specify which props are actually used
  return <SomeOtherComponent callback={myMemoizedCallback} />
  // now SomeOtherComponent will only rerender when onlyPropNeeded changes
}
Enter fullscreen mode Exit fullscreen mode

HoCs are out of fashion, for good reasons and you wont see them used anymore in most well known npm packages for React. You also won't see documentation on HoCs in the new React beta documentation that they're working on. Your example does not explain why you want to use 3 hooks to achieve the same functionality as your HoC... Infact it's not clear what you're trying to achieve with that HoC in the first place.

Collapse
 
zelofff profile image
zelofff

Do you use React.memo in every component? If not, there will be re-render anyways

const Parent = () => {
  const [count, setCount] = useState(0)
  const memoizedProp = useMemo(() => {
     return { id: 123 }
  }, [])

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <Child prop={memoizedProp} />
    </>
  )
}

const Child = (props) => {
  console.log('rerender', props)

  return <p>non-memoized child</p>
}
Enter fullscreen mode Exit fullscreen mode

Try this example in codesandbox and you'll see that Child will rerender on the button click, so useMemo doesn't do anything in that case.

Collapse
 
brense profile image
Rense Bakker

Thats not what useMemo is for. useMemo is for calculating derived state. But you are right yes, if the parent rerenders, the children will rerender, unless you create a memoized component. The problem with not memoizing props that you pass to components is that it changes the internal state of that component on every rerender, which can result in further rerenders due to side effects and/or cause unexpected behavior inside child components because they cannot rely on the props ever being referentially equal. For example, all your side effects and memoized derived state will always trigger/change on every render.

Ask yourself this: do you think the people at Facebook got bored and decided to add a completely useless useCallback hook to react?

Thread Thread
 
zelofff profile image
zelofff

I'm not saying that useCallback is useless. My point is that it shouldn't be used everywhere, you should understand and know why you want use it. Dependant effects in child component is a good case, when you could use useCallback.

Thread Thread
 
brense profile image
Rense Bakker

The point is, you dont know what happens in the child component. If the parent needs to know what goes on in the child, you do not have seperation of concerns, which is a bad practice. Therefor if you pass anything to the props of a child component, you should always make sure it is memoized, so the child can do with those props whatever it wants, because it can rely on that very important contract between the parent and the child, that the props will ONLY change if the value is actually different. This is the responsibility of the parent component. If you cannot rely on this, you need to keep a mental model of your entire app in your head at all times and if you forget anything while fixing a problem in a child component, you will be confronted with debugging nightmare that gets progressively worse, the further down the tree you are.