DEV Community

Maycon Sousa
Maycon Sousa

Posted on

Optimize React functional components with Hooks

There's no doubt that Hooks are one of the most exciting features of the last React updates. They let we work in a project without writing class-based components, allowing use of state and other features.

One important concern that we have to do when developing applications, in general, is performance.

React already has a "diffing" algorithm to avoid unnecessary DOM render, but in some cases, we want to avoid unnecessary executions of the component's render function to increase performance. In the case of functional components, render function is itself.

I created the following project to demonstrate how we can optimize React functional components with Hooks:

1. The application

Alt Text

This application is simple!

  • Home is the root component;
  • Component1 displays the current name;
  • Component2 displays the current surname;
  • The root component has a input field for name and another for surname;
  • The root component stores the name and surname in a local state (using useState hook);
  • The root component pass down the property name to Component1 and surname to Component2;

Code:

// ./src/pages/index.tsx

import React, { useState } from 'react';

import { Component1, Component2 } from '../components';

export default function Home() {
  const [name, setName] = useState('');
  const [surname, setSurname] = useState('');

  return (
    <div className="container">
      <label>Name: </label>
      <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
      <label>Surname: </label>
      <input type="text" value={surname} onChange={(e) => setSurname(e.target.value)} />
      <Component1 name={name} />
      <Component2 surname={surname} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// ./src/components/Component1.tsx

import React from 'react';

interface Props {
  name: string;
}

export default function Component1({ name }: Props) {
  console.log('Component1 :: render', { name });

  return (
    <div>
      <label>Component1: </label>
      <p>Name: {name}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// ./src/components/Component2.tsx

import React from 'react';

interface Props {
  surname: string;
}

export default function Component2({ surname }: Props) {
  console.log('Component2 :: render', { surname });

  return (
    <div>
      <label>Component2: </label>
      <p>Surname: {surname}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. The first problem

I put a console.log in the Component1 and Component2 to print the properties on them.

Alt Text

So, after typing my name, see what happened!

Alt Text

Component2 prints the console.log message indicating that it was executed unnecessary. The surname property value is empty all the time.

2.1. Solution

To resolve this problem, we just need to use React.memo!

React.memo is a higher-order component and it allows a component to be rendered only if the properties are changed.

// ./src/components/Component2.tsx

...

function Component2({ surname }: Props) {
  ...
}

export default React.memo(Component2);
Enter fullscreen mode Exit fullscreen mode

So, after the change...

Alt Text

3. The second problem

See what happened when I added a property data of the type object in the Component2.

// ./src/components/Component2.tsx

import React from 'react';

interface Props {
  surname: string;
  data: Record<string, unknown>;
}

function Component2({ surname, data }: Props) {
  console.log('Component2 :: render', { surname, data });

  return (
    <div>
      <label>Component2: </label>
      <p>Surname: {surname}</p>
      <p>Data: {JSON.stringify(data)}</p>
    </div>
  );
}

export default React.memo(Component2);
Enter fullscreen mode Exit fullscreen mode
// ./src/pages/index.tsx

...

<Component2 surname={surname} data={{}} />
Enter fullscreen mode Exit fullscreen mode

Alt Text

Alt Text

Component2 prints the console.log message indicating that it was executed unnecessary.

AGAIN !!!

Even if I declare the following way, same problem occurs...

// ./src/pages/index.tsx

...

const data = {};

...

<Component2 surname={surname} data={data} />
Enter fullscreen mode Exit fullscreen mode

Why ???

How to resolve this?

3.1. Solution

One thing about React.memo is that, by default, it will only shallowly compare complex objects in the props object.

Well, every time that the root component renders because state changes, a new instance of object {} was created and pass down to Component2. The shallow comparison of the React.memo detects that the object is different and re-render the Component2.

To resolve this problem, React provides a hook called useMemo. This function receives two arguments, a "create" function and an array of dependencies. useMemo will only execute the "create" function to return a new instance of the data when one of the dependencies has changed.

Let's update the code...

// ./src/pages/index.tsx

import React, { useMemo, useState } from 'react';

...

const data = useMemo(() => ({ surname }), [surname]);

...

<Component2 surname={surname} data={data} />
Enter fullscreen mode Exit fullscreen mode

Alt Text

It's all OK now!

4. The last problem

See what happened when I added a property func of the type function in the Component2.

// ./src/components/Component2.tsx

import React from 'react';

interface Props {
  surname: string;
  data: Record<string, unknown>;
  func: () => void;
}

function Component2({ surname, data, func }: Props) {
  console.log('Component2 :: render', { surname, data, func });

  return (
    <div>
      <label>Component2: </label>
      <p>Surname: {surname}</p>
      <p>Data: {JSON.stringify(data)}</p>
    </div>
  );
}

export default React.memo(Component2);
Enter fullscreen mode Exit fullscreen mode
// ./src/pages/index.tsx

...

<Component2 surname={surname} data={data} func={() => undefined} />
Enter fullscreen mode Exit fullscreen mode

Alt Text

Alt Text

Component2 still prints the console.log message...

The reason is the same as the previous topic. A new instance of the passed function is created every time that the state changes.

4.1. Solution

To resolve this problem, React provides a hook called useCallback. This function receives two arguments, a function and an array of dependencies. The operation is similiar to useMemo. useCallback will only create a new instance of the function when one of the dependencies has changed.

The final code...

import React, { useCallback, useMemo, useState } from 'react';

import { Component1, Component2 } from '../components';

export default function Home() {
  const [name, setName] = useState('');
  const [surname, setSurname] = useState('');

  const data = useMemo(() => ({ surname }), [surname]);

  const func = useCallback(() => undefined, []);

  return (
    <div className="container">
      <label>Name: </label>
      <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
      <label>Surname: </label>
      <input type="text" value={surname} onChange={(e) => setSurname(e.target.value)} />
      <Component1 name={name} />
      <Component2 surname={surname} data={data} func={func} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

That's all folks!

Top comments (2)

Collapse
 
munizzinum profile image
Philippe Muniz

Great Job dude!
Thank's for sharing!

Collapse
 
olsard profile image
olsard

Great, thanks for sharing!