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.