DEV Community

Rahman Fadhil
Rahman Fadhil

Posted on • Edited on • Originally published at rahmanfadhil.com

Optimize React Hooks Performance

Read the original article here

According to the official React documentation, 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.

Let's get started by initializing a React application with Create React App.

$ npx create-react-app app-name

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

// ./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.

// ./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 called React.memo. Which is very similar to React.PureComponent 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

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

// ./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.

// ./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.

// ./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 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.

// ./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.

// ./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 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.

// ./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.

Top comments (5)

Collapse
 
pyeongoh profile image
pyeong_oh

your last exmaple or example with useCallback for prevent re-rendering is not working, when every input changed, counter component is re-rendered.

If you want prevent re-rendering counter compnent when input value changed, changed the useCallback's sencond array paramter [value] to []

Input changes means value is changed, so useCallback always return new addHello, counter component receive new referenced function so it always re-rendered, that is why your last example is not working.

Collapse
 
rahmanfadhil profile image
Rahman Fadhil

Hmm, I don't think so...

We don't use useCallback to prevent re-rendering counter component. We use it to memoize our function (the one that add "Hello!" to the "value" state). When you memoize your function with useCallback, the component is still being re-rendered but the returned value of the memoized function is cached. This is very helpful if you want to do expensive task that requires a lot of time.

We put [value] because we always want to update our function when the "value" state changes, because we need "value" state inside our function. So, our function is still being memoized unless the "value" state has changed.

But thanks for the feedback anyway 😃

Collapse
 
liuxinqiong profile image
Ethan

Thanks

Collapse
 
alabobriggs profile image
Alabo David Briggs

After memoising the Counter component with useCallback and useMemo is there still a need to use React.memo?

Collapse
 
rahmanfadhil profile image
Rahman Fadhil

Yes, if you want to pass data to the child component via props.