Kalan's Blog

Kalan 頭像照片,在淡水拍攝,淺藍背景

四零二曜日電子報上線啦!訂閱訂起來

Software Engineer / Taiwanese / Life in Fukuoka
This blog supports RSS feed (all content), you can click RSS icon or setup through third-party service. If there are special styles such as code syntax in the technical article, it is still recommended to browse to the original website for the best experience.

Current Theme light

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

Please notice that currenly most of posts are translated by AI automatically and might contain lots of confusion. I'll gradually translate the post ASAP

Several use cases of useMemo

Several Use Cases of useMemo

In frontend applications, it is common to encounter situations where the displayed values are calculated or processed from other values. For example:

  • Time: converting seconds into the format xx:xx for minutes and seconds display.
  • If a filter option is selected, the 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 nothing wrong with this approach. However, for scenarios where we calculate values before displaying them, I prefer using useMemo to express the intention more explicitly:

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

In this example of time formatting, the difference between the two approaches is not significant. The original approach is already concise, and the computation is not heavy. Using useMemo might seem like over-optimization. However, in real-world scenarios where the number of dependencies increases, using useMemo allows developers to perform calculations within the function, avoiding confusion caused by excessive use of ternary operators. When other developers see useMemo, they can quickly understand that "Oh, this calculation depends on these variables and produces another variable."

Some might argue, "What's the difference compared to wrapping it in a function?" For example:

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

In my opinion, unless this calculation is a very generic function, wrapping the calculation in a function only reduces the number of lines in the component on the surface. Developers still need to dive into the function's implementation to understand what it does. In that case, it is better to directly use useMemo and write the implementation inside it.

Computation Depends on Multiple States

Another use case is when data depends on other filters, such as:

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, the filtered state changes, and the data is filtered based on whether it is a favorite or not. One drawback of this approach is that the data processing logic is written within the JSX, making it harder to maintain when the logic becomes more complex. Therefore, we can extract it into a single 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 simplifies the expression in JSX, but the implementation inside the filteredData variable declaration might be less intuitive. In this example, the conditional statement is still relatively simple. However, as mentioned earlier, as the number of dependencies increases, the code becomes more complex.

In such cases, I prefer to move the computation 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. In that case, we can simply add the corresponding processing and dependencies within useMemo:

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]) // Assuming data changes only when the length changes
  return <div>
    <button onClick={() => setFiltered(s => !s)}>toggle filtered</button>
    {filterData.map(...)}
  </div>
}

This approach clarifies the dependencies, allowing developers to quickly understand which variables the data depends on.

Derived State

Another use case is "derived state," meaning the state itself depends on the changes in props. Let's modify the previous example:

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 to update the component state whenever the query changes. However, this approach has two problems:

  • Whenever the data changes, an unnecessary state update occurs because we update the state again.
  • The input value depends on both the prop and the state, making it challenging to maintain a single source of truth.

The most dangerous aspect for developers is the inability to maintain a single source of truth, which is non-intuitive and difficult to debug.

In addition to the two mentioned problems, this approach is not correct because the argument in useState(value) defines the initial value. Even if the value changes, the state won't change. This means that even if data or query updates, filteredData will still have the value from the initial render.

This is because the argument in useState only takes effect during the initial render. The correct (but not recommended) approach would be:

/* Not recommended */
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 a prop using useState, in most cases, it can be rewritten using useMemo. In this example, it is similar to the previous example and can be rewritten as:

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 above examples, we can summarize a few points:

  • Computed props can be optimized with useMemo, reducing the cognitive load for other developers when reading the code.
    • In cases with heavy computation, it can also save the cost of re-computation on each render.
  • Be cautious when using useState(props).
  • In most cases, derived state can be solved using useMemo.

Furthermore, one significant advantage of useMemo is that it helps express the intention of the code. Although it may seem like overhead (which it actually is, as other frameworks don't memoize like React), sacrificing a bit of performance for readability is still worthwhile.

The React beta documentation discusses whether to use useMemo everywhere, and I think it's worth considering.

Prev

Change from prismjs to shiki

Next

How to determine if the owners of two domains are the same?

If you found this article helpful, please consider buy me a drink ☕️ It'll make my ordinary day shine✨

Buy me a coffee