Optimize React Hooks Performance
Tags: javascript, react, performance
Published: 2019-06-04 11:59:06

_Read the original article [here](https://rahmanfadhil.com/optimize-react-hooks)_

According to the official React [documentation](https://reactjs.org), **Hooks are functions that let you "hook into" React state and lifecycle features from function components**. Which means you now have full control of your functional components, just like the other class-based components.

So, if you have a good understanding of what React Hooks are, check out this simple React application.

## Getting started

I have published a similar project on my GitHub, you can clone it right [here](https://github.com/rahmanfadhil/learn-hooks).

Let's get started by initializing a React application with [Create React App](https://facebook.github.io/create-react-app).

```bash
$ npx create-react-app app-name
```

Then, edit the `./src/App.js` file.

```jsx
// ./src/App.js

import React, { useState } from "react"
import Counter from "./Counter"

export default function App() {
  const [value, setValue] = useState("")

  return (
    <div>
      <input
        type="text"
        onChange={e => setValue(e.target.value)}
        value={value}
      />
      <Counter />
    </div>
  )
}
```

In the `App` component, we are using the `Counter` component imported from `./src/Counter.js` file, which we have not created yet. Let's fix it by creating it.

```jsx
// ./src/Counter.js

import React, { useState, useRef } from "react"

export default function Counter() {
  const [counter, setCounter] = useState(0)
  const renders = useRef(0)

  return (
    <div>
      <div>Counter: {counter}</div>
      <div>Renders: {renders.current++}</div>
      <button onClick={() => setCounter(counter + 1)}>Increase Counter</button>
    </div>
  )
}
```

In this example, there are two functional components. First, is the `App` component which contains `useState` hook to control the input value.

Second, is the `Counter` component which contains `useState` hook to hold the counter state and `useRef` hook to count how many times this component updated or re-rendered.

Try to run the app, and play around with it. You shouldn't see a performance issue at the moment. So, let's find out if there is any problem with it.

## One big problem

When you press the 'Increase Counter' button several times, the renders counter shows the exact same number as the counter state. Which means the `Counter` component updated whenever our counter state changed.

But when you type in the `App` component text input, you will see that the renders counter also increased. Which means that our `Counter` component rerendered whenever our text input state changed.

So, how can we fix it?

## Memoizing components

React 16.6 (and higher) comes with [higher order component](https://reactjs.org/docs/higher-order-components.html) called [`React.memo`](https://reactjs.org/docs/react-api.html#reactmemo). Which is very similar to [`React.PureComponent`](https://reactjs.org/docs/react-api.html#reactpurecomponent) but for functional component instead of classes.

Basically, it **helps us control when our components rerender**.

> "In computing, memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again" ― [Wikipedia](https://en.wikipedia.org/wiki/Memoization)

Let's memoize our `Counter` component to prevent unnecessary rerender.

```jsx
// ./src/Counter.js

import React, { useState, useRef } from "react"

export default React.memo(() => {
  const [counter, setCounter] = useState(0)
  const renders = useRef(0)

  return (
    <div>
      <div>Counter: {counter}</div>
      <div>Renders: {renders.current++}</div>
      <button onClick={() => setCounter(counter + 1)}>Increase Counter</button>
    </div>
  )
})
```

Easy right? Let's checkout out our new app and you'll see that the `Counter` component isn't re-rendered when we type in the text input.

## The problem persists

`React.memo` is great. But, the problem is not solved yet.

There is **one thing to always remember** when memoizing React component. When the parent component passed props to a memoized React components, things get a little bit weird.

When passing **numbers or strings** as props, memoized components will check whether the props are changed or not. The component will only rerender when the numbers of strings are changed.

But when passing **functions or objects**, memoized components will always rerender when the parent component rerender. This happens because whenever the parent component passes that kind of data, memoized components **couldn't check whether that function or object are changed or not**.

To prove this, let's try to pass a prop to `Counter` component.

```jsx
// ./src/App.js

import React, { useState } from "react"
import Counter from "./Counter"

export default function App() {
  const [value, setValue] = useState("")

  return (
    <div>
      <input
        type="text"
        onChange={e => setValue(e.target.value)}
        value={value}
      />
      <Counter greeting="Hello world!" />
    </div>
  )
}
```

In this case, we pass `greeting` prop which contains a string. Then, try to run the app and you'll see that our app will run as we expected. Now, try to pass a function or object.

```jsx
// ./src/App.js

import React, { useState } from "react"
import Counter from "./Counter"

export default function App() {
  const [value, setValue] = useState("")

  return (
    <div>
      <input
        type="text"
        onChange={e => setValue(e.target.value)}
        value={value}
      />
      <Counter
        addHello={() => setValue(value + "Hello!")}
        myObject={{ key: "value" }}
      />
    </div>
  )
}
```

You will notice that your `Counter` rerender whenever you type something in the text field. So, how can we fix this... Again?

## Memoizing functions

We can use [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback) hook to memoize our callback that we pass through the props.

`useCallback` hook returns a memoized version of our function that only changes if one of the dependencies have changed. In other words, our function will never be recreated unless the state value has changed. Let's implement this in our app.

```jsx
// ./src/App.js

import React, { useState, useCallback } from "react"
import Counter from "./Counter"

export default function App() {
  const [value, setValue] = useState("")

  const addHello = useCallback(() => setValue(value + "Hello!"), [value])

  return (
    <div>
      <input
        type="text"
        onChange={e => setValue(e.target.value)}
        value={value}
      />
      <Counter addHello={addHello} myObject={{ key: "value" }} />
    </div>
  )
}
```

This method is **very useful when you have more than one state hook**. The memoized functions are updated only when the chosen state changed. To prove this, let's add another input field.

```jsx
// ./src/App.js

import React, { useState, useCallback } from "react"
import Counter from "./Counter"

export default function App() {
  const [value, setValue] = useState("")
  const [newValue, setNewValue] = useState("")

  const addHello = useCallback(() => setValue(value + "Hello!"), [value])

  return (
    <div>
      <input
        type="text"
        onChange={e => setValue(e.target.value)}
        value={value}
      />
      <input
        type="text"
        onChange={e => setNewValue(e.target.value)}
        value={newValue}
      />
      <Counter addHello={addHello} myObject={{ key: "value" }} />
    </div>
  )
}
```

Now, when we type in the new text field, the `Counter` component doesn't rerender. Because our memoized function only being updated whenever the `value` state has changed.

We are successfully memoized our function with the `useCallback` hook. But, the problem still persists though...

## Memoizing objects

Now we know how to memoize our function, but there is one last thing you should know about memoizing.

Currently, our `Counter` component is still rerendered whenever the state has changed. Its because the `myObject` props are still not memoized yet. So, how can we memoize that kind of stuff?

[`useMemo`](https://reactjs.org/docs/hooks-reference.html#usememo) hook let you memoize a value (including objects) by passing a "create" function and an array of dependencies. The value will only recompute when one of the dependencies has changed (just like `useCallback` hook).

Let's apply that and see what happened.

```jsx
// ./src/App.js

import React, { useState, useCallback } from "react"
import Counter from "./Counter"

export default function App() {
  const [value, setValue] = useState("")
  const [newValue, setNewValue] = useState("")

  const addHello = useCallback(() => setValue(value + "Hello!"), [value])
  const myObject = useMemo(() => ({ key: "value" }), [])

  return (
    <div>
      <input
        type="text"
        onChange={e => setValue(e.target.value)}
        value={value}
      />
      <input
        type="text"
        onChange={e => setNewValue(e.target.value)}
        value={newValue}
      />
      <Counter addHello={addHello} myObject={myObject} />
    </div>
  )
}
```

By adding these changes, you're now able to pass props to a memoized component without losing good performance.