Go back

React performance: Memoization

May 17, 2025 ยท 4 min read

I want this to be a series of articles where I'd share what I've been learning about performance in React for a while now. But I absolutely cannot assure myself that I'll be up to that task. When I find or run into any "gotcha" or "haha" perf moments while building stuff, I'll try linking whatever I write about here.

There's a back story to this one;

When I started learning React, one fun thing I enjoyed about doing so was the ability to write something once and re-use it everywhere. One specific UI component I enjoy building is the <List /> or anything that had to do with using an iterator function like Array.prototype.map() to render stuff.

By design, we've always been encouraged to define these lists of items outside the component itself, for some time, i did not fully understand why. But, as time passed, i found out subtly, that it is a way to improve the component(?), not the performance, per se.

Say we have a list of items for a Sidebar component.

const SIDEBAR_ITEMS = [
  { id: crypto.randomUUID(), name: "People", path: "/people" },
  { id: crypto.randomUUID(), name: "Settings", path: "/settings" },
  ...more stuff
]
 
export const Sidebar = () => {
  return (
    <>
      {SIDEBAR_ITEMS.map((item) => {
        return (
          <a href={item.path} key={item.id}>
            <li className="fancy-item">
              {item.name}
            </li>
          </a>
        )
      })}
    </>
  )
}

The snippet above is pretty much a skeletal structure of what is required to render your fancy sidebar, styled to your taste, of course!

Dynamic-ness

But, here comes the dealbreaker; What if, for some reason, in the future, we need to somehow mutate how this list is rendered in our UI based on some props the sidebar component receives?

Piece of cake! Too easy, move SIDEBAR_ITEMS into the component โ€” and you'll be correct to say that. So let's do just that.

Now the initial snippet becomes this, because we want to somehow, for instance (do not mind my example), change the path value for the 'Settings' sidebar item, if the user has a different role.

export const Sidebar = ({ user }: { user: User }) => {
  const SIDEBAR_ITEMS = [
    { id: crypto.randomUUID(), name: "People", path: "/people" },
    {
      id: crypto.randomUUID(),
      name: "Settings",
      path: user.role === "fiery_chicken" ?
        "/settings/fire" :
        "/settings"
    },
    ...more stuff
  ]
 
  return (
    <>
      {SIDEBAR_ITEMS.map((item) => {
        return (
          <a href={item.path} key={item.id}>
            <li className="fancy-item">
              {item.name}
            </li>
          </a>
        )
      })}
    </>
  )
}

Everything seems alright. Great! Problem solved.


Not quite! In the snippet above, every local variable โ€” SIDEBAR_ITEMS in this case โ€” in that component will be re-created on every render, which is something you do not want most of the time.

Because everything in JavaScript is simply an object in memory, and we use references to access them. Even the Sidebar component's existence is due to React.createElement, which is referenced as this:

{
  "$$typeof": Symbol.for("react.element"),
  type: React.Fragment,
  key: null,
  ref: null,
  props: {
    children: SIDEBAR_ITEMS.map((item) => ({
      "$$typeof": Symbol.for("react.element"),
      type: "a",
      key: item.id,
      ref: null,
      props: {
        href: item.path,
        children: {
          "$$typeof": Symbol.for("react.element"),
          type: "li",
          key: null,
          ref: null,
          props: {
            className: "fancy-item",
            children: item.name
          },
          _owner: null
        }
      },
      _owner: null
    }))
  },
  _owner: null
}

Enter useMemo and useCallback

These hooks carry out the same function โ€” caching. The subtle difference between both of them is this;

useMemo() helps us cache the value or result of a computation between re-renders, while useCallback() helps us with caching a function definition between re-renders. You'll find the latter commonly used when, say, for example, you want a function to run when certain props/values change in a component.

In our case, we need to ensure SIDEBAR_ITEMS is cached during renders; So, the second component snippet becomes

export const Sidebar = ({ user }: { user: User }) => {
  const SIDEBAR_ITEMS = React.useMemo(() => [
    { id: crypto.randomUUID(), name: "People", path: "/people" },
    {
      id: crypto.randomUUID(),
      name: "Settings",
      path: user.role === "fiery_chicken" ?
        "/settings/fire" :
        "/settings"
    },
    ...more stuff
  ], [user.role])
 
  return (
    <>
      {SIDEBAR_ITEMS.map((item) => {
        return (
          <a href={item.path} key={item.id}>
            <li className="fancy-item">
              {item.name}
            </li>
          </a>
        )
      })}
    </>
  )
}

Ah, you read up to this point. Thank you, I guess?

When I continue down my React performance trope path, I'll try to find time to update this article with whatever I write next. Thanks again!