Coming from angular, i would like to know what’s wrong with this code. I’ve been told that i should use useMemo (for the fitlering part) and useRef (to store products).
I know how to do that, but i want to better understand what’s wrong with this.
I have compared the number of renderings whenever searchTerm changes, and i see no difference.
export function ProductList({ productService }) {
const [products, setProducts] = useState([])
const [filteredProducts, setFilteredProducts] = useState([])
const [searchTerm, setSearchTerm] = useState(null)
function fetchData() {
productService
.allProducts()
.then((products) => {
setProducts(products)
setFilteredProducts(products)
})
.catch(console.error)
}
function filterProductsByName(searchTerm) {
setSearchTerm(searchTerm)
setFilteredProducts(filterByName(products, searchTerm))
}
useEffect(fetchData, [])
return (
<div>
<h1>Products</h1>
<form onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="searchTerm">Search</label>
<input
type="text"
name="searchTerm"
id="searchTerm"
onChange={(e) => filterProductsByName(e.target.value)}
/>
</div>
</form>
<div>
{filteredProducts.map((p) => (
<Product key={p.id} product={p} />
))}
</div>
</div>
)
}
I have compared both versions (with an without useMemo)
2
Answers
There nothing wrong with your code. But in case the filtering is rather expensive, you want to avoid it to be done too often – i.e. when the page gets rerendered for other reasons than a changed filter term. At the moment, when anything changes at the current page, the filtering will be re-applied. These changes are not even required in your current component to happen. Keep in mind how React rerenders:
state
updates anywhere.useEffect
,useMemo
,useCallback
) will be executed again, even the expensive ones.So,
ensures the filtering won’t be run again as long as
searchTerm
remains the same, especially not when it’s just another component which receives a state update.But in practise, I wouldn’t over-optimize here. If the list to be filtered isn’t insanely long, there’s no need to introduce more complicated code until it harms performance. It’s even more error-prone with
useMemo
: in case you change your component at some time so it fetches new items not only initially but also time-based or based on an interaction and just adds them to the existing ones, a memorized value would suddenly bear wrong results. Long story short: avoid unnecessary complexity.Using
useMemo
to manage derived state is good advice, using a ref to store state … I’d question that decision. There are arguments to do this, but that’s a separate discussion.This ain’t an issue of performance but one where people can’t always keep track of all the possible states they may find themselves in.
This is a fairly simple component, so it’s fairly simple to update the
filteredProducts
when you load theproducts
and whenever thesearchTerm
changes, right? What about this:when you load the
products
you set thefilteredProducts
to the same list. That implies that there is nosearchTerm
. So you assume that this request will happen so fast that nobody would have time to enter asearchTerm
before you set theproducts
… not even the browsers autocomplete? And what about a slow connection?Even in this simple code, you’ve already overlooked a possibility which may lead to
filteredProducts
not reflecting thesearchTerm
. It’s unlikely/rare, but possible.OK, then we’ll simply
setFilteredProducts(filterByName(products, searchTerm));
and things are fine, right? ButsearchTerm
in this context is a closure over the initial value, not its current value, so this doesn’t work either.Do you see the issue?
The trivial solution would be to just do
but this may compute unnecessarily often, so we
useMemo
to only recompute that list when its dependencies change:that’s the "textbook solution";
Then again, this is a pretty simple component. Tell me, what besides loading the
products
or changing thesearchTerm
would trigger a rerender of this component? Doesn’t that mean that you have to compute thefilteredProducts
on basically ever render anyways?useMemo
is usually cheaper than the operation done within, but it ain’t free.I’d suggest you try
One source of unnecessary rerenders may be the parent element. If that is the case you may want to consider React.memo().
Also take a look on the topic of "controlled vs uncontrolled input" in react, maybe start here: React – Controlled Components.