DEV Community

Cover image for How to optimize your React Components using Hooks
Daniel Silva
Daniel Silva

Posted on • Edited on

How to optimize your React Components using Hooks

When developing, it's important to make good, reusable, and fast components. Here, we'll check some ways to make your app to go faster than ever. But, to do this, we have to understand how React works and how we should create our components to make them faster.

How React works with renders

When a state changes in a component, it will re-render again (Simple components lifecycle), but what not all developers know is that it will also re-render every single one of their children's components. Why is that? Because with every render of the component, it will send props to their children in some kind of domino effect.
This is the normal React behavior but ¿What if we have a component that doesn't change any of their props values? It will re-render? The answer is yes! But this is not always the best for the performance of the app.
Let's imagine we have a form with one input and there's also an static image:

import React, { useState } from 'react';
import { Logo } from './Logo.js';
import { Input } from './Input.jsx';

export const MyComponent = () => {
   const [value, setValue] = useState(null);

   return (
      <Logo size={300} />
      <Input type="text" value={value} onChange={setValue} />
   )
}
Enter fullscreen mode Exit fullscreen mode

The only prop that the Logo component is receiving is a number and will never change, but the keypress of the keyboard will make MyComponent change its status and re-render with its children. This has no sense because that unnecessary render from the Logo component will make the performance of your application go down, but don't worry, we will fix this.

Pure functions are the way

A pure function is a function that has to accomplish two things:

  1. With the same entry values, it will return the same result.
  2. When executed, it won't have any side effects on other values. A good example is:
const sum = (a,b) => a + b;

sum(2,2) /* will return 4 */
Enter fullscreen mode Exit fullscreen mode

Doesn't matter how many time we call sum(2,2) will always return 4. In this way, React has something called Pure Components for classes components or memo() for functional components, that acts just like a pure function, only re-rendering a component when their props change.

Mastering the memoization

Memoization is a way to allocate in memory the results of a function call to be used again if needed to avoid going to the execution again and optimizing the call times if the result will be the same. So, if working with pure functions, this will be perfect to avoid unnecessary execution that will return the same value.

This technique will work with functional components as well and, as we are working with Hooks and functional components, we will be working with the memo function that will be provided to us by React and is just as easy as wrapping our component with the memo function.
Let's see how to fix our previous problem memorizing our Logo component:

// Logo.ts

import React, { memo } from 'react';
import { Images } from './images.js';

const LogoUnmemoized = ({size}) => (
   <img src={images.logo} width={size} />
);

export const Logo = memo(LogoUnmemoized);
Enter fullscreen mode Exit fullscreen mode

That's it! Now your component will not do unnecessary re-renders by props passing.

But components are not the only thing we can allocate in memory, but also do it with functions, and here React provide us two hooks to use, useCallback and useMemo.

useCallback and useMemo

useCallback and useMemo are a way to memoize functions depending on how they work and will be written almost the same way, receiving a callback function and a dependency array. This dependency array is the one that works exactly as the useEffect dependency array, checking if it's different and if it's needed to re-create the function.
The useCallback will work on functions that will not return anything but to call another function(s), for example:

const mainFunction = () => useCallback(() => {
     console.log("this")
     // This will not have a return value
}, [/* dependencies */]);
Enter fullscreen mode Exit fullscreen mode

And, useMemo will work on functions that will return a specific value. We can take the same sum pure function that we use before:

const sum = useMemo((a,b) => a + b, [/* dependencies */]);

sum(2,2); /* 4 and memorize this value. */
sum(2,2); /* Also 4, but will not execute the function and will take the same value memoized before. */
sum(4,4); /* 8 and delete the previous memoized value and store this one */
Enter fullscreen mode Exit fullscreen mode

Those two functions will also avoid unnecessary re-renders on children's components, therefore optimizing the app, but there's nothing free or perfect in life, and memoization it's not the exception.


With great power comes great responsibilities.
-Ben Parker


While this can look great, memoizing costs a lot, so we have to be careful and learn what functions/components can or cannot be stored in memory.
If a component will have their props changed a lot in a short amount of time shouldn't be allocated in memory because it will do this memory allocation a lot of times and, besides optimizing our app, will take the performance ground floor. The same thing happens with the functions we're calling if the variables declared on the dependency arrays will be changing a lot, it's highly recommended don't use useMemo nor useCallback

Pro tip: Use debounce

Let's imagine we have a search bar on our application, and with every keypress, it will do a new search, making unnecessary requests to the server because users keep typing more than one letter.
Well, we can improve the performance of the app in this case by using debounce. This is used to request the server when the user stops typing for some amount of time. For example, if the user stops typing for 0,3 seconds will make the request. If not, it will wait until they stop typing and pass the time.
This could not be a lot of time, but this makes a lot of difference and will improve the performance of the app by avoiding unnecessary backend calls.

Here's an easy implementation of debounce using hooks:

import { useEffect, useState } from 'react';

export default function useDebounce(value: string, delay: number) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

Enter fullscreen mode Exit fullscreen mode

I do this completely non-profit, but if you want to help me you can go here and buy me a coffee ;)

Top comments (2)

Collapse
 
aminya profile image
Amin Yahyaabadi • Edited

Using Solid-js you can reduce the size of your program from 200KB to 4KB and increase the runtime performance by at least 2 times. Spending time optimizing React is not worth it.

rawgit.com/krausest/js-framework-b...

Collapse
 
ucvdesh profile image
Daniel Silva

Yeah, I know there are other options out there, but when you have to work on a React app (maybe the client ask for that technology or maybe there's an existing app to work on) so you have to know how to do it right! ;)