← Blog

Why Search Uses an Old Answer in React Native

5 min read
Watch the walkthrough on YouTube

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. I reviewed the snippets against the source and did not execute them on a device. They are integration examples rather than 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. The old suggestion can cause this failure even when responses arrive in 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. Your app supplies the routing callbacks. 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().

Below 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 only demonstrates AbortController and doesn't add 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. Passing the debounced input would compare the answer with the delayed value again. TanStack Query's cancellation guide explains how its signal reaches fetch.

The limitation

This guard protects the redirect decision. Stale suggestions can still render, and repeated identical queries need their own freshness handling. Keep destination validation in the routing layer.

Steps to check in your app

These are suggested checks. I have not run the snippets from this article.

  • 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. The screen should show results without redirecting or throwing.
  • 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.

Comments

No account needed.

  1. Loading comments…