Performance Optimization

Performance Optimization

React.memo

React.memo stores a component, and only re-renders if the props of the component change (it memoizes the component). In the next example, that means that we only re-render BigList if products change, thus, we do not re-render any SingleProduct component unless products change.

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

// Custom hook
import { useFetch } from "useFetch";

const url = "https://course-api.com/javascript-store-products";

const Index = () => {
  const { products } = useFetch(url);
  const [count, setCount] = useState(0);

  return (
    <>
      <h1>Count : {count}</h1>
      <button className="btn" onClick={() => setCount(count + 1)}>
        click me
      </button>
      <BigList products={products} />
    </>
  );
};

// Each time a prop or the state changes, the component re-renders, so all
// the elements of the list are processed again.
// However if we use React.memo we only re-render the component if products change

const BigList = React.memo(({ products }) => {
  return (
    <section className="products">
      {products.map((product) => {
        return <SingleProduct key={product.id} {...product}></SingleProduct>;
      })}
    </section>
  );
});

const SingleProduct = ({ fields }) => {
  let { name, price } = fields;
  price = price / 100;
  const image = fields.image[0].url;

  return (
    <article className="product">
      <img src={image} alt={name} />
      <h4>{name}</h4>
      <p>${price}</p>
    </article>
  );
};

export default Index;

useCallback

What happens if we pass a function to BigList, well if the state changes (whichever variable of the state) then the function is created again, and so the function is different. Which means the props of BigList list changes, and causes React.memo to re-render the entire component. That is why we use useCallback.

useCallback allows us to define when to create a function, by specifying the dependencies like we did with useEffect:

  • If the dependency is []: then only create in the first render
  • If there are variables in the []: create whenever those variables change
  • If there is nothing: create always.

Refer to Customs Hooks for an use case of useCallback inside the custom hook useFetch.

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

// Custom hook
import { useFetch } from "useFetch";

const url = "https://course-api.com/javascript-store-products";

const Index = () => {
  const { products } = useFetch(url);
  const [count, setCount] = useState(0);
  const [cart, setCart] = useState(0);

  // We only create this function when we update the cart value
  // That is we memoize the function
  const addToCart = useCallback(() => {
    setCart(cart + 1);
  }, [cart]);

  return (
    <>
      <h1>Count : {count}</h1>
      <button className="btn" onClick={() => setCount(count + 1)}>
        click me
      </button>
      <BigList products={products} addToCart={addToCart} />
    </>
  );
};

// Each time a prop or the state changes, the component re-renders. Because now
// addToCart is define with useCallback, the re-render is not triggered

const BigList = React.memo(({ products, addToCart }) => {
  return (
    <section className="products">
      {products.map((product) => {
        return (
          <SingleProduct
            key={product.id}
            {...product}
            addToCart={addToCart}
          ></SingleProduct>
        );
      })}
    </section>
  );
});

const SingleProduct = ({ fields, addToCart }) => {
  let { name, price } = fields;
  price = price / 100;
  const image = fields.image[0].url;

  return (
    <article className="product">
      <img src={image} alt={name} />
      <h4>{name}</h4>
      <p>${price}</p>
      <button onClick={addToCart}>add to cart</button>
    </article>
  );
};

export default Index;

useMemo

Note that this hook deals with values (which is the traditional functionality of the idea of memoizing), whilst React.memo look for changes in the props.

In the next example we create a function that returns a value, and we memoize the function, so it only computes the value whenever the products change (the argument of the function), else it returns the value stored before:

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

// Custom hook
import { useFetch } from 'useFetch'

const url = 'https://course-api.com/javascript-store-products'

// Define the function we are going to memoize
const calculateMostExpensive = (data) => {
  return (
    data.reduce((total, item) => {
      const price = item.fields.price
      if (price >= total) {
        total = price
      }
      return total
    }, 0) / 100
  )
}

const Index = () => {
  const { products } = useFetch(url);
  const [count, setCount] = useState(0);
  const [cart, setCart] = useState(0);

  const addToCart = useCallback(() => {
    setCart(cart + 1)
  }, [cart])

  // Memoize the function with useMemo
  const mostExpensive = useMemo(() => calculateMostExpensive(products), [
    products,
  ])

  return (
    <>
      <h1>Count : {count}</h1>
      <button className='btn' onClick={() => setCount(count + 1)}>
        click me
      </button>
      <!-- Show most expensive product -->
      <h1>Most Expensive : ${mostExpensive}</h1>
      <BigList products={products} addToCart={addToCart}/>
    </>
  )
}

const BigList = React.memo(({ products, addToCart }) => {

  return (
    <section className='products'>
      {products.map((product) => {
        return (
          <SingleProduct
            key={product.id}
            {...product}
            addToCart={addToCart}
          ></SingleProduct>
        )
      })}
    </section>
  )
})

const SingleProduct = ({ fields, addToCart }) => {
  let { name, price } = fields
  price = price / 100
  const image = fields.image[0].url

  return (
    <article className='product'>
      <img src={image} alt={name} />
      <h4>{name}</h4>
      <p>${price}</p>
      <button onClick={addToCart}>add to cart</button>
    </article>
  )
}

export default Index;