· 6 min read

A Few Use Cases for useMemo

This article was auto-translated from Chinese. Some nuances may be lost in translation.

Several Use Cases for useMemo

In frontend applications, we often encounter scenarios where a value displayed on the screen is computed from other values, or undergoes some calculation before being rendered. For example:

  • Time: converting seconds into an xx:xx minute-second format for display
  • If a filter option is checked, data is processed before being displayed

Using It as a Computed Prop

We can write it like this:

const FormatTime = (ts) => {
  const formattedTime = `${Math.floor(ts/60)}:${ts%60}`
  return <time>{formattedTime}</time>
}

There is not really anything wrong with writing it this way, but for scenarios like “calculate something before displaying it”, I prefer using useMemo to further convey intent:

const FormatTime = (ts) => {
  const formattedTime = useMemo(() => {
    return `${Math.floor(ts/60)}:${ts%60}`
  }, [ts])
  
  return <time>{formattedTime}</time>
}

In this time formatting example, the difference between the two isn’t huge. The original approach is already quite concise and the calculation is light, so using useMemo might seem like premature optimization. However, in real-world scenarios, as dependencies grow, using useMemo allows developers to perform computations inside a dedicated function body, avoiding the clutter of nested ternary operators. When other developers see useMemo, they can also immediately realize: “Ah, this calculation depends on these few variables to derive another variable.”

Some might argue: how is that any different from wrapping it in a helper function? For example:

const Component = (ts) => {
  const computed = calculateMyProp(ts)
  return <div>...</div>
}

My view is that unless this computation is a highly generic function, wrapping it in an external function merely reduces the line count of the component on paper. Developers maintaining the code still have to jump to that function’s implementation to see what it does. In that case, you might as well use useMemo and keep the implementation right there.

Computations Dependent on Multiple States

Another use case is when data depends on other filters, like this:

const MyComponent = ({ data }) => {
  const [filtered, setFiltered] = useState(false)
  
  return <div>
    <button onClick={() => setFiltered(s => !s)}>toggle filtered</button>
    {data.filter(d => filtered ? d.favorite : true).map(...)}
  </div>
}

When the user clicks the button, it toggles filtered. When filtered is true, it filters out favorite items. The drawback here is that data processing logic is embedded inside the JSX, which becomes harder to maintain as logic grows more complex. Therefore, we can extract it into a separate variable:

const MyComponent = ({ data }) => {
  const [filtered, setFiltered] = useState(false)
+ const filteredData = data.filter(d => filtered ? d.favorite : true)
  return <div>
    <button onClick={() => setFiltered(s => !s)}>toggle filtered</button>
+   {filterData.map(...)}
  </div>
}

This makes the expression inside the JSX much simpler, but the implementation inside filteredData’s variable declaration still looks slightly unintuitive. While this condition is relatively simple in this case, as mentioned earlier, it becomes increasingly complex as dependencies increase.

At this point, I would move the calculation into useMemo:

const MyComponent = ({ data }) => {
  const [filtered, setFiltered] = useState(false)
  const [sorted, setSorted] = useState(false)
  const filteredData = useMemo(() => {
    if (filtered) {
      return data.filter(d => d.favorite)      
    }

    return data
  }, [data.length, filtered])
  return <div>
    <button onClick={() => setFiltered(s => !s)}>toggle filtered</button>
  	{filterData.map(...)}
  </div>
}

Suppose we add another option, sorted, today. Inside useMemo, all we need to do is add the corresponding logic and dependencies:

const MyComponent = ({ data }) => {
  const [filtered, setFiltered] = useState(false)
  const [sorted, setSorted] = useState(false)
  const filteredData = useMemo(() => {
    const origin = [...data]
    if (filtered) {
      return origin.filter(d => d.favorite)      
    }

    if (sorted) {
      return data.sort()
    }

    return origin
  }, [data.length, filtered, sorted]) // 這邊假設 data 的變化只發生在長度有變化時
  return <div>
    <button onClick={() => setFiltered(s => !s)}>toggle filtered</button>
    {filterData.map(...)}
  </div>
}

The benefit of this approach is that the dependencies are declared very clearly—developers can see at a glance which variables this data depends on.

Derived State

Another use case is “derived state”—state that depends on prop changes. Let’s rewrite the example above:

const MyComponent = ({ data }) => {
  const [query, setQuery] = useState(value)
  const [filteredData, setFiltered] = useState(data.filter(d => query ? d.includes(query) : true))
  return <>
    <input onChange={e => setQuery(e.target.value)} value={query} />
  	{filteredData.map(...)}
  </>
}

We want the component state to update whenever query updates. However, there are two issues with this approach:

  • Whenever data changes, it requires another state update, causing an unnecessary re-render.
  • The derived value relies on both props and state simultaneously, breaking the single source of truth.

The most dangerous aspect for development is losing the single source of truth, as it is very unintuitive and extremely difficult to debug.

Beyond the two issues mentioned above, this pattern is actually incorrect because the argument to useState(value) defines only the initial value—meaning state won’t change even if value changes. Consequently, even when data or query updates later, filteredData will still retain its value from the initial render.

Because useState’s argument is only applied on the initial render. A working (though not recommended) approach would be:

/* 不推薦此寫法 */
const MyComponent = ({ data }) => {
  const [query, setQuery] = useState(value)
  const [filteredData, setFiltered] = useState(data.filter(d => query ? d.includes(query) : true))
+ useEffect(() => { data.filter(d => d.includes(query)) }, [data, query])
  return <>
    <input onChange={e => setQuery(e.target.value)} value={query} />
  	{filteredData.map(...)}
  </>
}

When you declare a state that depends on props, in most cases it can be rewritten using useMemo. Taking this example, it’s quite similar to the earlier case and can be rewritten with useMemo:

const MyComponent = ({ data }) => {
  const [query, setQuery] = useState(value)
  const filteredData = useMemo(() => {
    if (query) {
      return data.filter(d => d.includes(query))
    }
    
    return data
  }, [data, query])
	
  return <>
    <input onChange={e => setQuery(e.target.value)} value={query} />
  	{filteredData.map(...)}
  </>
}

Summary

From the examples above, we can summarize a few key takeaways:

  • Computed props can use useMemo to help reduce cognitive load for other developers reading the code.
    • In cases with heavy computation, it can also save the cost of recomputing on every render.
  • Be especially cautious when using useState(props).
  • In most cases, derived state can be handled with useMemo.

Additionally, for me, a major advantage of useMemo is helping code express its intent. While it might look like a bit of overhead (and honestly, it really is XD; other frameworks don’t make you memoize like this), trading a tiny bit of performance for readability is well worth it.

The React beta docs feature a discussion on whether to use useMemo everywhere, which I think is well worth reading.

Related Posts

Explore Other Topics