Redundant re-renders in React
Tags: react, performance, javascript, webdev
Published: 2022-10-18 17:38:36

## Table of Contents
 * [When components re-render?](#when-components-rerender)
 * [How to prevent re-render] (#how-to-prevent-rerender)
   * [`useCallback` and `useMemo`] (#use-callback-use-memo)
   * [`React.memo`] (#react-memo)
   * [Move state down] (#move-state-down)
   * [Move content up] (#move-content-up)
   * [HOC for loading] (#hoc-for-loading)
   * [Uncontrolled input] (#uncontrolled-input)
 * [Wrap up] (#wrap-up)


---

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? <a name="when-components-rerender"></a>

Look at the example below
```ts
const Child = () => <p>some text</p>

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

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

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` <a name="use-callback-use-memo"></a> <a name="how-to-prevent-rerender"></a>

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` <a name="react-memo"></a>

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

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

export const MemoizedComponent = React.memo(Component)
```

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 <a name="move-state-down"></a>

Let's imagine you have this component:
```ts
const Parent = () => {
  const [count, setCount] = useState(0)

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

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:
```ts
const Union = () => {
  const [count, setCount] = useState(0)

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

const Parent = () => {
  return (
    <>
      <Union />
      <ComponentC />
    </>
  )
}
```

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

## Move content up <a name="move-content-up"></a>

Let's complicate the previous example
```ts
const Parent = () => {
  const [count, setCount] = useState(0)

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

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.

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

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

After we moved component A, component B, and their state up, we can pass component C as a child in `Union` component.
```ts
const Parent = () => {
  return (
    <Union>
      <ComponentC />
    </Union>
  )
}
```

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

## HOC for loading <a name="hoc-for-loading"></a>

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

For example:
```ts
const Parent = ({ isLoading }) => {
  const someCb = useCallback(...)
  const someCalculatedValue = useMemo(...)

  useEffect(...)

  if (isLoading) {
    return <Loader />
  }

  return ...
}
```

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.
```ts
const WithLoading = (Component) => {
  return ({ isLoading, ...props }) => {
    if (isLoading) {
      return <Loader />
    }

    return <Component {...props} />
  }
}
```

Then we can use `WithLoading` HOC like this.
```ts
const Parent = () => { ... }

export const WithLoadingParent = WithLoading(Parent)
```

## Uncontrolled input <a name="uncontrolled-input"></a>

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.

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

  const handleSearch = () => {
    console.log(inputRef.current.value)
  }
	
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleSearch}>Search</button>
    </>
  )
}
```

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 <a name="wrap-up"></a>

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.