If you have any questions or feedback, pleasefill out this form
This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.
Several Use Cases for useMemo
In front-end applications, it's common to encounter situations where the values displayed on the screen are derived from other values through complex calculations, or where values are computed before being rendered. For example:
- Time: converting seconds into the
xx:xx
minutes and seconds format - If a filter option is selected, the data will be processed before being displayed
Using 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>
}
This approach is actually not problematic, but for scenarios where "calculations are performed before display," I prefer to use useMemo
to express intent more clearly:
const FormatTime = (ts) => {
const formattedTime = useMemo(() => {
return `${Math.floor(ts/60)}:${ts%60}`
}, [ts])
return <time>{formattedTime}</time>
}
In the case of formatting time, the difference between the two approaches isn't significant since the original method is already quite concise and the computational load is minimal. Using useMemo
might seem like over-optimization. However, in real scenarios where the dependencies of a value increase, useMemo
allows developers to perform calculations within functions, thereby avoiding confusion caused by excessive use of ternary
operators. Additionally, when other developers see useMemo
, they can quickly understand that "this calculation depends on these variables and produces another variable."
Some might argue, "Isn't that the same as wrapping it in a function?" For example:
const Component = (ts) => {
const computed = calculateMyProp(ts)
return <div>...</div>
}
In my view, unless the calculation is a very generic function, wrapping it as a function only reduces the line count on the surface; developers still need to dig into the function's implementation to see what it does. It might be better to use useMemo
and keep the implementation directly within it.
Dependencies on Multiple States
Another use case involves data that 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 changes the filtered
state. When filtered
is true
, it filters for favorite items. One downside of this approach is that the data processing logic is embedded within the JSX, making it harder to maintain when the logic becomes complex. Thus, 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>
+ {filteredData.map(...)}
</div>
}
This makes the expression within the JSX simpler, but the implementation of the filteredData
variable declaration might still seem a bit unintuitive. In this case, the condition is straightforward, but as mentioned earlier, as dependencies grow, the code can become increasingly complex.
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>
{filteredData.map(...)}
</div>
}
Assuming we add another option sorted
, we can simply include the corresponding processing and dependencies in 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 changes to data occur only when the length changes
return <div>
<button onClick={() => setFiltered(s => !s)}>toggle filtered</button>
{filteredData.map(...)}
</div>
}
The advantage of this approach is that the declaration of dependencies becomes very clear, allowing developers to quickly see which variables the data relies on.
Derived State
Another use case is "derived state," meaning the state itself depends on changes in props. We can rewrite the earlier example like this:
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's state to update whenever the query changes. However, this approach has two issues:
- Every time
data
changes, we unnecessarily update the state again, leading to an extra update. - The input value simultaneously relies on both prop and state, making it difficult to maintain a single source of truth.
One of the most dangerous aspects for developers is the inability to maintain a single source of truth, which can be counterintuitive and very difficult to debug.
In addition to the two issues mentioned, this code is also fundamentally incorrect because the parameter of useState(value)
defines the initial value. This means that even if value
changes, the state will not. Consequently, even if data
or query
updates afterward, filteredData
will still hold the value from the first render.
The parameter of useState
is only effective during the first render. The correct (but not recommended) way to write it 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(() => { setFiltered(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, it can usually be rewritten using useMemo
. In this example, it's quite similar to the previous cases and can be rewritten using useMemo
as follows:
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(...)}
</>
}
Conclusion
From the examples above, we can summarize a few points:
- Computed props can leverage
useMemo
to reduce cognitive load for other developers reading the code.- In cases where computation is intensive, it can also save the cost of recomputing on every render.
- Special caution is needed when using
useState(props)
. - Derived state can usually be addressed using
useMemo
.
Additionally, for me, a significant advantage of useMemo
is that it helps express intent within the code. While it may seem like a bit of overhead (and it really is XD; other frameworks don’t bother with memoization), sacrificing a little performance for readability is still quite worthwhile.
There is a discussion in the React beta documentation regarding whether to use useMemo everywhere, which I find quite worth referencing.
If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨
☕Buy me a coffee