← Blog

Why Search Uses an Old Answer in React Native

5 min read

Type shoe, wait for a suggestion, add s, and press Search. The input says shoes, but the app opens Shoe Care, the destination suggested for the earlier word.

I use a query-match check in one of my React Native apps to protect this decision. This lesson recreates the pattern with an illustrated phone and sample data. The article snippets are written and reviewed against the source, not executed. They are integration examples, not a complete runnable app.

Never act on a suggestion for a word you didn’t submit. Match the response to the request before using it.

Why debounce doesn’t prevent this

A suggestion response can include the word it belongs to, matching items, and an optional destination. The screen stores that answer while you continue typing.

The previous lesson covered debounce: wait for a pause before starting the next request. That delay doesn’t replace an answer already in state.

The illustrated sequence is:

  1. Request suggestions for shoe. After 2,000 ms, store an answer with query: "shoe" and redirectUrl: "/shoe-care".
  2. Add s. The input becomes shoes; the stored suggestion still belongs to shoe.
  3. Submit immediately, inside the 500 ms debounce window.
  4. The new suggestion request starts later and takes 300 ms. Its eventual answer cannot undo a navigation decision already made.

The delays are sample values. This failure does not require responses to arrive out of order.

Check the query at submit

trim() removes spaces at the beginning and end. Compare the submitted term with the query attached to the suggestion before using its redirect.

type Suggestion = {
  query: string;
  redirectUrl: string | null;
};

type SearchActions = {
  openKnownPage: (url: string) => boolean;
  showResults: (term: string) => void;
};

function submitSearch(
  query: string,
  suggest: Suggestion | undefined,
  actions: SearchActions,
) {
  const term = query.trim();
  if (!term) return;

  const redirect =
    suggest?.query.trim() === term ? suggest.redirectUrl : null;

  // Your routing adapter returns true only for a supported destination.
  if (redirect && actions.openKnownPage(redirect)) return;
  actions.showResults(term);
}

"shoe" does not equal "shoes", so this submission falls back to results for shoes. A matching suggestion can still supply a supported destination. The routing callbacks are supplied by your app; they are not React Native APIs. Keep your existing destination validation in the routing adapter.

Use the same normalization when sending and comparing queries. This example only trims whitespace. It deliberately does not add case folding or Arabic normalization.

Cancellation solves a different part of the problem

Cancellation can stop a fetch that is still running. It cannot erase a completed answer that your screen already remembers. That distinction is why I keep the submit-time comparison. MDN documents the scope of abort().

Here is a small transport helper. Replace the example URL with your API and validate its payload using your project’s normal response parser.

async function fetchSuggestion(query: string, signal: AbortSignal) {
  const response = await fetch(
    `https://api.example.com/suggest?q=${encodeURIComponent(query)}`,
    { signal },
  );
  if (!response.ok) throw new Error(`Suggestion failed: ${response.status}`);

  const payload: unknown = await response.json();
  if (!payload || typeof payload !== 'object') {
    throw new Error('Invalid suggestion response');
  }
  const redirectUrl = (payload as Record<string, unknown>).redirectUrl;
  if (redirectUrl !== null && typeof redirectUrl !== 'string') {
    throw new Error('Invalid redirectUrl');
  }
  // Label the answer with the exact query used by this request.
  return { query, redirectUrl };
}

function startSuggestion(query: string) {
  const controller = new AbortController();
  return {
    promise: fetchSuggestion(query, controller.signal),
    cancel: () => controller.abort(),
  };
}

The caller must handle rejection from promise, including cancellation, and call cancel() when that request is no longer needed. This helper demonstrates AbortController; it is not a second state-management layer.

For server data managed by TanStack Query, pass its supplied signal into that same transport function. This hook takes the debounced term from the previous lesson and assumes your app already has a QueryClientProvider.

import { keepPreviousData, useQuery } from '@tanstack/react-query';

function useSuggestions(debouncedTerm: string) {
  return useQuery({
    queryKey: ['suggest', debouncedTerm],
    queryFn: ({ signal }) => fetchSuggestion(debouncedTerm, signal),
    enabled: debouncedTerm.length > 0,
    placeholderData: keepPreviousData,
  });
}

The query key separates answers for different words. keepPreviousData intentionally keeps the previous answer visible during a transition. Even without that option, the debounce window can leave the hook on the old term until the delayed value changes. Pass data into the guarded submit handler alongside the current input, not the debounced input. TanStack Query’s cancellation guide explains how its signal reaches fetch.

The limitation

This guard protects the redirect decision. It doesn’t prevent every stale suggestion from rendering, guarantee the newest result for repeated identical queries, or validate a destination by itself. Rendering freshness and route validation remain separate responsibilities.

Steps to check in your app

These are suggested checks; I have not run these article snippets.

  • Give shoe a 2,000 ms delay and a /shoe-care redirect. Give shoes a 300 ms delay and no redirect.
  • Wait for the shoe answer, add s, and submit immediately. With the guard, expect results for shoes.
  • Submit shoes before the slow answer has arrived, with no suggestion in state. Expect results, not a redirect or an exception.
  • Wait for the shoes answer and submit again. Expect results for shoes.
  • Submit shoe with its matching answer in state. Expect the supported Shoe Care destination.
  • Repeat with surrounding spaces and an empty input. Spaces should be trimmed; an empty search should do nothing.
  • Cancel a pending request and handle its rejection. Then repeat after an answer has already been stored: cancellation doesn’t remove that stored answer.

Previous: React Native search debounce.

Next: Keep Typing While Search Loads. The link will be added when that lesson is published.

Comments

No account needed — your first comment creates an anonymous name and a secret key.

  1. Loading comments…