All answers

React Native

How do I filter a list with a search bar in React Native?

Keep the search text in state, filter your data with it inside useMemo using a case-insensitive includes, and render the filtered array in a FlatList. When the data lives on a server, wait until the user pauses typing, then run the query with the search text.

Updated October 31, 2026

If you have a list on screen and want a search bar above it that narrows the results as people type, it’s less code than it looks. The idea is to keep what the user typed in state and work out the matching items from it on every render, so the list only ever gets those. Filters like a category or a price range are the same idea with one more condition.

How it works

There are two cases. When the whole list is already in the app, we’d filter it on the phone, which is the simplest option and updates with every keystroke. When the data is large or only lives on a server, send the search text to the server instead, and wait for a short pause in typing so you aren’t sending a request for every letter.

How to build it

  1. Add a search field that stores what the user types. These props give it a search keyboard and stop the phone from capitalizing or autocorrecting the search text:

    const [query, setQuery] = useState("");
    
    <TextInput
      value={query}
      onChangeText={setQuery}
      placeholder="Search"
      inputMode="search"
      returnKeyType="search"
      autoCapitalize="none"
      autoCorrect={false}
      clearButtonMode="while-editing"
    />;

    clearButtonMode adds a clear button on iOS only.

  2. Filter the list with useMemo, lowercasing both sides so “apple” matches “Apple”. Add your filters as extra conditions in the same function:

    const filtered = useMemo(() => {
      const q = query.trim().toLowerCase();
      return items.filter((item) => {
        const matchesText =
          !q || [item.name, item.city].some((field) => (field ?? "").toLowerCase().includes(q));
        const matchesCategory = category === "all" || item.category === category;
        return matchesText && matchesCategory;
      });
    }, [items, query, category]);
  3. Render the result in a FlatList with an empty state:

    <FlatList
      data={filtered}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <ItemRow item={item} />}
      keyboardShouldPersistTaps="handled"
      ListEmptyComponent={<Text>No results for "{query}"</Text>}
    />

    keyboardShouldPersistTaps="handled" is easy to leave out, but with the default, the first tap on a result while the keyboard is open only closes the keyboard, and the row never receives the tap.

  4. For server data, debounce the query and cancel the previous request when the text changes. Here’s the same search with Supabase:

    useEffect(() => {
      const controller = new AbortController();
      const timer = setTimeout(async () => {
        const { data } = await supabase
          .from("products")
          .select("id, name")
          .ilike("name", `%${query.trim()}%`)
          .abortSignal(controller.signal);
        if (data) setResults(data);
      }, 300);
    
      return () => {
        clearTimeout(timer);
        controller.abort();
      };
    }, [query]);

    It’s worth cancelling, because a slow request for “ap” can otherwise finish after the one for “apple” and replace the right results with stale ones.

How you know it works

Typing part of a name shows only the matching rows, whatever the case, and clearing the field brings back the full list. Typing something that matches nothing shows your empty message, and tapping a result with the keyboard still open opens it on the first tap.

If the results still look wrong

Check for fields that can be empty, since calling toLowerCase() on null crashes the filter, which is why the example uses field ?? "". If the list never updates, confirm you’re passing filtered, not the original items, to the FlatList. And in a server search, % and _ in what the user types act as wildcards in ilike, so a search for “50%” can match more rows than you expect.

Ask the agent

If your app is in Draftbit, paste this into the agent, and it can read your code and make the change for you.

Add a search bar above the list on this screen that filters it as I type, matching the name and city without caring about upper or lower case. Add category filter chips next to it, and show a friendly message when nothing matches.

Open Draftbit

More React Native answers