javascript 54 lines · 8 steps

Memoizing a filtered list in React

A search-and-sort product list that recomputes its visible items only when the inputs that matter actually change.

Explained by highlit
1import { useMemo, useState } from 'react';
2 
3function ProductList({ products }) {
4 const [query, setQuery] = useState('');
5 const [minRating, setMinRating] = useState(0);
6 const [sortBy, setSortBy] = useState('relevance');
7 
8 const visibleProducts = useMemo(() => {
9 const term = query.trim().toLowerCase();
10 
11 const filtered = products.filter((product) => {
12 if (product.rating < minRating) return false;
13 if (!term) return true;
14 return (
15 product.name.toLowerCase().includes(term) ||
16 product.tags.some((tag) => tag.toLowerCase().includes(term))
17 );
18 });
19 
20 if (sortBy === 'price') {
21 filtered.sort((a, b) => a.price - b.price);
22 } else if (sortBy === 'rating') {
23 filtered.sort((a, b) => b.rating - a.rating);
24 }
25 
26 return filtered;
27 }, [products, query, minRating, sortBy]);
28 
29 return (
30 <div className="product-list">
31 <input
32 type="search"
33 value={query}
34 placeholder="Search products"
35 onChange={(e) => setQuery(e.target.value)}
36 />
37 <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
38 <option value="relevance">Relevance</option>
39 <option value="price">Price</option>
40 <option value="rating">Rating</option>
41 </select>
42 <p>{visibleProducts.length} results</p>
43 <ul>
44 {visibleProducts.map((product) => (
45 <li key={product.id}>
46 {product.name} ${product.price.toFixed(2)}
47 </li>
48 ))}
49 </ul>
50 </div>
51 );
52}
53 
54export default ProductList;
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Derive displayed data from state inside render rather than storing a second copy you have to keep in sync.
  2. 2Wrap expensive list transforms in useMemo with an honest dependency array so they rerun only when an input changes.
  3. 3Controlled inputs feed state, state feeds the memo, and the memo feeds the UI in one predictable flow.

Related explainers

javascript
const ROLE_PERMISSIONS = {
  admin: ['users:read', 'users:write', 'billing:read', 'billing:write'],
  manager: ['users:read', 'billing:read'],
  member: ['users:read'],

Role-based permissions middleware in Express

authorization middleware rbac
Intermediate 9 steps
javascript
function attachThousandSeparators(input, { locale = 'en-US' } = {}) {
  const formatter = new Intl.NumberFormat(locale);
  const groupSep = formatter.format(11111).replace(/\d/g, '')[0] || ',';
  const decimalSep = formatter.format(1.1).replace(/\d/g, '')[0] || '.';

Live thousand separators without losing the caret

dom intl caret-preservation
Advanced 8 steps
javascript
import { useReducer, useEffect } from "react";
 
const initialState = { status: "idle", data: null, error: null };
 

Building a data-fetching hook in React

custom-hooks usereducer data-fetching
Intermediate 9 steps
javascript
const express = require('express');
const app = express();
 
app.get('/health', (req, res) => res.json({ status: 'ok' }));

Graceful shutdown in an Express server

graceful-shutdown signal-handling connection-tracking
Advanced 9 steps
javascript
import { useState, useEffect, useCallback } from 'react';
 
function getColumnCount(width) {
  if (width < 640) return 1;

A responsive column hook in React

custom-hooks debouncing responsive-design
Intermediate 7 steps
javascript
function initCharacterCounter(textarea, options = {}) {
  const maxLength = options.maxLength ?? 280;
  const warnThreshold = options.warnThreshold ?? 0.9;
 

A live character counter for textareas

dom closures accessibility
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

Memoizing a filtered list in React — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code