Optimizing React Components with useCallback() hook

Next.js code for creating React applications with functional components and React hooks for managing life-cycles
Photo by Juanjo Jaramillo / Unsplash

Optimization is a crucial part of any application. In React applications this include to mitigate unnecessary renders as well as reducing the rendering time itself. In this article, we'll see how the React useCallback hook can help us to make our application to preform better.

Memoization

Let's first understand what memoization is and why is important. Memoization, simply put, is the programming technique to cache the result of a previous function call along with its dependencies. This technique allows an application to quickly pull out a cached result if the input values of the memoized function were already seen before. Therefore, the application can run faster in most cases.

Function equality in JavaScript

In JavaScript, we can treat functions just like any other object; we can compare functions, pass functions as arguments, return a function from inside another function, assign a function's reference as a value to a variable, and pretty much anything you can do with an object (thus, functions are first-class in JavaScript).
So, in order to understand the need for memoization in React, let's implement a factory function, which will return a function. And then, let's use it to create two others functions and see if those reference the same object in memory.

// factory
const factory = () => 
  (base: number, exponent: number) => Array(exponent)
    .fill(base)
    .reduce((acc, curr) => acc * curr)

const funcRef1 = factory()
const funcRef2 = factory()

// they both must always return the same value when inputs are the same
funcRef1(2, 3) // expected output 8
funcRef2(2, 3) // expected outout 8

console.log(funcRef1 === funcRef2) // expected output false

The useCallback hook

As we could see in the previous example, every time we called the factory function a different reference was created. This is exactly the behavior we get from React when a re-render happens. React will re-create each and every functions' references inside of a component on every re-render.

With the React useCallback hook, we can memoize a function and get an instance from it which will only change if one of the dependencies changes. This means React will use the same function's reference between renders instead of re-creating it.

const memoizedFunction = useCallback(
 // the callback function to be memoized
 () => { // code },
 // dependencies array
 []
)

Use case scenario

Imagine we have the typical and always present in every React project component which renders a list of child components; and we want to optimaze those child components preventing unnecessary re-renders. In order to accomplish such goal, we have to, first, memoize the child components with React.memo(), and finally, see if we are passing down functions' references as props to the child components. In that case, we also want to memoize each of those functions with the useCallback() hook

It's important you realize that React.useCallback(cb, dependencies) by itself won't be enough. You also need to memoize the component which receives the memoized function

useCallback hook in action

Before jumping into the action, let's first, introduce the example we're about to explore: Next.js will be our React framework, and we want to render a list of professors. Each of those professors can be marked as favorite. For this example, we will only render three professors (let's keep it simple and focus on how to use the useCallback hook)
Our goal, of course, is to re-render only the professor we are marking as favorite.

src/components/molecules/ProfessorDetail.tsx

import React from "react"
import { FunctionComponent, MouseEvent } from "react"
import { styled, VariantProps } from "@stitches/react"
import { FaHeart } from "react-icons/fa"

export interface IProfessor {
  id: strin
  name: string
  bio: string
  favorite: boolean
}

export interface IProfessorDetailProps extends IProfessor,
    VariantProps<typeof ProfessorDetailStyled> {
  onFavorite: (id: string) => void
}

const ProfessorDetailStyled = styled("div", {
  // base styles
  display: "flex",
  flexDirection: "column",
  alignItems: "center",
  padding: "18px 36px",
  gap: "20px",
  boxShadow: "0 0 1px 1px #e6e6e6",

  "& > img": {
    borderRadius: "50%",
    boxShadow: "0 0 1px 3px rgb(0,0,0,0.4)",
    backgroundColor: "rgb(0.4, 0.4, 0.4, 0.3)",
  },

  "& > h3": {
    width: "75%",
    marginTop: "15px",
    display: "flex",
    justifyContent: "space-between",
  },

  ".icon-heart": { cursor: "pointer" },

  "& > p": {
    "-webkit-hyphens": "auto",
    "-moz-hyphens": "auto",
    "-ms-hyphens": "auto",
    hyphens: "auto",
    alignSelf: "start",
    textIndent: "24px",
  },

  variants: {
    favorite: {
      true: {
        "& .icon-heart": { fill: "#e63900" },
      },
      false: {
        "& .icon-heart": { fill: "#4d4d4d" },
      },
    },
  },

  defaultVariants: {
    favorite: false,
  },
})


const ProfessorDetail: FunctionComponent<IProfessorDetailProps> = (props) => (
  <ProfessorDetailStyled className="professor_detail" favorite={props.favorite}>
  <img 
    src={`/images/${props.id}.jpeg`} 
    width="180px" 
    height="180px" 
  />
  
  <h3>
    <span>{props.name}</span>
    <FaHeart 
      className="icon-heart" 
      onClick={() => onFavorite(props.id)} 
    />
  </h3>
  <p>{props.bio}</p>
  </ProfessorDetailStyled>
)

export default React.memo(ProfessorDetail)

Notice I'm using React.memo() which is a React higher-order component to wrap the ProfessorDetail component. By doing so, React memoizes the rendered output of the wrapped component.
As mentioned before, this step is crucial since the useCallback hook does not do the work by itself.
In our example, we can mark a professor as favorite. We do so by clicking in the heart icon which will execute the delegated function "onFavorite"

src/components/molecules/ProfessorsList.tsx

import { FunctionComponent, MouseEvent, useCallback, useEffect } from "react"
import { styled } from "@stitches/react"
import { useProfessorsReducer } from "../../store/reducers/professor.reducer"
import ProfessorDetail, { IProfessor } from "./ProfessorDetail"

export const PROFESSORS: IProfessor[] = [
  {
    id: "96c27f9fd99ab603",
    name: "Marta Hatzell",
    bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Purus ut faucibus pulvinar elementum integer enim neque volutpat. Iaculis at erat pellentesque adipiscing commodo. Integer malesuada nunc vel risus commodo viverra maecenas accumsan lacus.",
    favorite: false,
  },
  {
    id: "b91b236542ce43b7",
    name: "Ralph Buehler",
    bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar pellentesque habitant morbi tristique senectus et netus et. Imperdiet sed euismod nisi porta lorem mollis aliquam ut porttitor. Aenean sed adipiscing diam donec adipiscing tristique risus.",
    favorite: true,
  },
  {
    id: "33121d3214d12699",
    name: "Madhav Marathe",
    bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consequat interdum varius sit amet mattis vulputate enim nulla aliquet. Nunc eget lorem dolor sed viverra ipsum. Cum sociis natoque penatibus et magnis dis.",
    favorite: false,
  },
]

const ProfessorsListStyled = styled("div", {
  display: "grid",
  gridTemplateColumns: "repeat(3, 1fr)",
  columnGap: "30px",
  padding: "30px 80px",
  maxWidth: "1500px",
  margin: "0 auto",
})

const ProfessorsLoadingStyled = styled("div", {
  display: "flex",
  justifyContent: "center",
  textTransform: "uppercase",
  color: "#5c5cd6",
  fontSize: "24px",
  fontWeight: "700",
  paddingTop: "48px",
})

const ProfessorsList: FunctionComponent = () => {
  const [{ professors, professorsFetchStatus }, dispatch] = useProfessorsReducer()

  const onFavoriteHandler = useCallback(
    (id) => dispatch({ type: "Toggle_Favorite", payload: id }),
    []
  )

  useEffect(() => {
    ;(async () => {
      await new Promise<void>((resolve) => setTimeout(resolve, 1500))
      dispatch({ type: "Insert_Professors", payload: PROFESSORS })
    })()
  }, [])

  if (professorsFetchStatus === "Loading")
    return <ProfessorsLoadingStyled>Loading</ProfessorsLoadingStyled>

  return (
    <>
      <ProfessorsListStyled className="professors_container">
        {Array.from(professors.entries()).map(([id, professor]) => (
          <ProfessorDetail key={id} {...professor} onFavorite={onFavoriteHandler} />
        ))}
      </ProfessorsListStyled>
    </>
  )
}

export default ProfessorsList

The above code provides the implementation of the parent component. This is the component which will render a list of professor components (the child components) and provide the actual implementation of the onFavorite function (onFavoriteHandler that is). Notice, we use the useCallback hook for memoizing the function we're passing down as props to the child component, in this case <ProfessorDetail> which we already memoized with React.memo()(just like we described earlier)
And that is it! React will now only re-render the child component where the onFavoriteHandler function is fired.
Mission accomplished and RESPECT++

Important note

Do not overuse the useCallback and React.memo() hooks since React will check in every re-render if something changed. This is the equivalent to componentDidUpdate function in React class-based components.
Use these hooks when they really make sense. And, always, put things in a balance. For example, if your <ParentComponent /> is only rendering a few <ChildComponent />(let's say 30 or 50 child components like the most) then useCallback and React.memo() won't do much and rather add complexity to your code.

Conclusion

useCallback hook can be used to memoize functions and optimize React child components that rely on reference equality. And when it's combined with React.memo() hook, we prevent unnecessary re-renders. Be careful and don't confuse React.memo() with React.useMemo(). Latter is used to memoize values while the former is used to memoize a React component.