mediumReact Native#127

Product Search with Filters

Prompt

Build a product search screen. Requirements:

  1. Render products from a mock list (name, price, category)
  2. Search by name (case-insensitive, debounced 300ms)
  3. Sort by: name A-Z, name Z-A, price low-high, price high-low
  4. Mark items with price <= 100 as "on sale"

Solution

export default function ProductSearch() {
  const [query, setQuery] = useState('')
  const [sort, setSort] = useState('name-asc')
  const [debounced, setDebounced] = useState('')
  useEffect(() => {
    const t = setTimeout(() => setDebounced(query), 300)
    return () => clearTimeout(t)
  }, [query])
  const filtered = PRODUCTS
    .filter(p => p.name.toLowerCase().includes(debounced.toLowerCase()))
    .sort((a, b) => {
      if (sort === 'name-asc') return a.name.localeCompare(b.name)
      if (sort === 'name-desc') return b.name.localeCompare(a.name)
      if (sort === 'price-asc') return a.price - b.price
      return b.price - a.price
    })
  return (
    <View style={{ padding: 16 }}>
      <TextInput value={query} onChangeText={setQuery} placeholder="Search..." style={{ borderWidth: 1, padding: 8, borderRadius: 4, marginBottom: 12 }} />
      <Picker selectedValue={sort} onValueChange={setSort}>
        <Picker.Item label="Name A-Z" value="name-asc" />
        <Picker.Item label="Name Z-A" value="name-desc" />
        <Picker.Item label="Price $" value="price-asc" />
        <Picker.Item label="Price $$" value="price-desc" />
      </Picker>
      <FlatList data={filtered} keyExtractor={p => String(p.id)} renderItem={({ item }) => (
        <View style={{ padding: 12, borderBottomWidth: 1, flexDirection: 'row', justifyContent: 'space-between' }}>
          <Text style={{ fontWeight: item.price <= 100 ? 'bold' : 'normal' }}>{item.name} {item.price <= 100 ? '🔥' : ''}</Text>
          <Text>${item.price}</Text>
        </View>
      )} />
    </View>
  )
}
Mentor's take

The point of this screen is the debounce done the React way: an effect keyed on query that schedules a setTimeout and returns clearTimeout as cleanup. Every keystroke re-runs the effect, cancelling the previous timer — the cancellation is the debounce. No lodash, no ref juggling.

The two-state split matters: query drives the controlled TextInput so typing echoes instantly; debounced drives the filter so the expensive work runs 300ms after the user pauses. Debounce the consumer of the value, never the controlled value itself — a debounced value prop makes the keyboard feel broken.

Filter-then-sort is deliberate ordering: .filter() returns a fresh array, so the subsequent .sort() — which sorts in place — mutates only the copy, never PRODUCTS. Sort straight on the source array and the "original" order is gone for every later render. The comparator is a flat if-chain over the four sort keys with localeCompare for strings and subtraction for numbers; a lookup table of comparators is the refactor when the options grow.

Red flag: storing the filtered/sorted result in state via another effect. Query, sort key, and source list fully determine the output — derive it in render, useMemo it if the catalog gets big.

Say it: "I debounce with an effect whose cleanup cancels the previous timer — the raw query stays controlled for instant echo, and the filtered list is derived, never stored."