The quickest way to add search to an app is to filter the list you already loaded.
To add search to an app, filter the rows the screen already has. One piece of state holds what the user typed, the list renders only the rows whose text contains it, and the results narrow on every keystroke. That is a real feature, it works with no signal, and it is a smaller job than adding user login.
The limit arrives sooner than people expect, so here it is first. Filtering in memory only works while the whole searchable set is already on the device. A mobile app search bar over a few thousand short rows is fine. Past roughly ten thousand rows, or once each row carries paragraphs rather than a name, the download and the memory give out long before the filter loop does, and search has to move to the server.
See where local search runs outThe short version
One word, two completely different jobs.
Searching a list in an app and searching a table on a server are not the same feature at two sizes. One is a filter over data you are already holding. The other is a request you send somewhere, wait for, and may get nothing back from. They fail differently, they cost differently, and the code has almost nothing in common.
Decide which one you are building before you draw the box. Starting local and moving later is normal and cheap. Standing up a search service for a list of two hundred gym classes is a bill and a moving part you did not need.
Where local search actually runs out
There is no published row count, because the row count is not the thing that breaks. The payload is. Filtering locally means the entire searchable set is in memory, which means you downloaded it, parsed it and are holding it. Two thousand rows of name, category and a short note is a few hundred kilobytes and nobody notices. A hundred thousand rows of the same shape is tens of megabytes on a phone that may be on a train.
The matching loop is the cheap half. Lowercasing and substring matching a few thousand short strings costs a millisecond or two on a current phone, well inside the sixteen milliseconds that a screen running at sixty frames per second gives you. At a hundred thousand rows with more text in each one you are into tens of milliseconds per keystroke, and the field starts trailing the typing.
So the working rule is a payload rule, not a row rule. Under about a megabyte of searchable text, filter on the device. Over that, or when the set grows with no ceiling, or when it holds anything this user is not allowed to see, search on the server. Measure your own payload before trusting anybody's threshold, including this one.
Try it
Can this list be searched on the phone?
Local search means holding every searchable row in memory. The payload decides that, not the row count on its own.
Local works, but measure it
2,000 rows at 300 bytes each is about 586 KB to download and hold before anybody searches. Comfortable on a new phone on wifi. Try it on an old one on mobile data first.
Filtering a list you already have
The whole feature is one state variable and one derived array. Keep the query in state, compute the visible rows from the full array and the query on each render, and hand that to the list. Do not store the filtered rows in state as well. Two copies means two sources of truth and an argument about which one is stale.
Match across more than one field, and normalise both sides. Lowercase the query and the value, trim the whitespace, strip accents if your data has them, and search the fields people actually remember: the customer, the invoice number, the tag, not only the title. A bar that matches titles alone is the most common reason somebody reports that search is broken when the code is doing exactly what it says.
Rendering then gets blamed for slowness that belongs to the list rather than the search. React Native's list components are virtualised, and its performance documentation is direct about the failure mode: when the list cannot render items fast enough, the user scrolls into blank space where rows should be. Giving items a fixed height so the list does not have to measure them, and keeping each row cheap to draw, buys more perceived speed than anything you do to the matching.
Searching a table on the server
Once the data stays on the server, search becomes a query and the app becomes a client that waits. The simplest version is a substring match in SQL. It needs no setup and it finds exactly what was typed. It also reads the table, ignores word endings, and will not match the word running when somebody types run.
A full text index is the next step and it is a real one. The PostgreSQL documentation puts the trade-off plainly: a full text search can be run without an index, but most applications find that too slow for anything beyond the occasional ad-hoc lookup, so practical use usually means creating one. An index stores the searchable text a second time in a searchable shape, and something has to keep it in step with the rows. That is a question of mobile app database design, not a setting you flip.
Two things change in the app itself. Results now arrive late, so the screen needs a loading state, an empty state, and a way to discard a response that a newer keystroke has already made irrelevant. Otherwise answers land out of order and the list flickers between them.
Every server query is also a permission question. Filter by what this user is allowed to see inside the query, never in the app after the rows arrive, or search becomes the fastest route into somebody else's records. And paginate from the first day: a search that returns everything it found is a payload problem wearing a text box.
PostgreSQL documentation, tables and indexes for text search
What makes in-app search feel right
Search is judged in the first fraction of a second, so the cheap wins are behavioural. Debounce server queries by roughly a quarter of a second so you are not firing one per keystroke. Do not debounce a local filter at all, because it is already faster than a finger. Show results narrowing as people type rather than hiding them behind a submit button.
The empty result is a screen, not a blank. Say what was searched for, offer the nearest thing you do hold, and give one tap back to the full list. Recent searches earn their space too: most people look for the same handful of things, and a tappable history removes the typing altogether.
Treat everything after that as app performance work, because that is what it is. Whether the keyboard covers the first result, whether the list keeps its scroll position when the query is cleared, whether the field survives a rotation with its text intact. None of it is search logic, and all of it is what people mean when they say the search is bad.
Which kind of search to build
| Approach | Suits | Works with no signal | Handles typos | Main cost |
|---|---|---|---|---|
| Filter the array in memory | a few thousand short rows | Yes | No | one afternoon |
| On-device database with an index | tens of thousands, offline | Yes | No | a schema to keep in step |
| Substring query on your server | any size, exact matches only | No | No | a round trip per search |
| Full text index on your server | long text, many rows | No | word endings, not typos | index upkeep and tuning |
| A hosted search service | large catalogues | No | usually | a bill that grows with use |
Putting search into an app you are building
Search is usually the second feature, not the first. You ship the list, people fill it, and somewhere around the three hundredth row somebody asks why they have to scroll. That is the right moment to build it, because by then you know which fields people actually search by, and that is the one thing you cannot guess up front.
Newly is an AI app builder. You describe the app in plain English and it writes a real React Native and Expo project you own, runs it on a cloud iPhone or Android simulator while it builds, and ships it to TestFlight and to Google Play internal testing. It costs $25 a month and there is no free plan. It does not ship a hosted search service and it is not tied to one backend, so how the rows are stored and queried stays your decision.
Make that decision before you write the query. Settle where the searchable data lives first, because everything above follows from that one answer, and it is the part that is expensive to change later.
Questions people ask about adding search
Hold the query in one piece of state, derive the visible rows from the full list and that query, and render the derived rows. If the data is already on the device, that is the whole feature. If it lives on a server, the same state variable drives a request instead of a filter, and you add a loading state, an empty state and a way to ignore stale responses.
Describe the list people need to find things in
Name the fields they search by and say where the rows live, then build the search around those two answers instead of around a text box.
Start building