React Native React Query

Server data behaves differently from local data. It lives on another computer, arrives late, grows stale, and needs refreshing. Hand-written loading code repeats the same pieces on every screen: a loading flag, an error message, a retry, and a cache. React Query, now published as TanStack Query, manages all of those pieces with a few lines.

Two Kinds of State

Client State                   Server State
─────────────────              ─────────────────────
Modal open or closed           Product list
Selected tab                   User profile
Typed text in a field          Order history
Owned by the app               Owned by the server
Use useState / Redux           Use React Query

Install and Set Up

npm install @tanstack/react-query

Create one client and wrap the app.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MainNavigator />
    </QueryClientProvider>
  );
}

Fetch Data with useQuery

A query needs a unique key and a function that returns data. React Query runs the function, tracks the status, and stores the result under the key.

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

function ProductList() {
  const { data, isLoading, isError, error, refetch } = useQuery({
    queryKey: ['products'],
    queryFn: getProducts,
  });

  if (isLoading) return <ActivityIndicator />;
  if (isError) return <Text>{error.message}</Text>;

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => String(item.id)}
      renderItem={({ item }) => <Text>{item.title}</Text>}
    />
  );
}

Compare this with the manual version. The three state variables, the effect, and the try and catch blocks all disappear.

The Cache

Query keys act as labels on cache shelves. Two screens that request ['products'] share one stored result and trigger one network call.

Screen A asks ['products'] ──┐
                             ├──► Cache shelf "products" ──► one request
Screen B asks ['products'] ──┘

Later visit: cached data shows instantly,
fresh data loads quietly in the background

Keys with Variables

Include every variable that changes the result inside the key.

useQuery({
  queryKey: ['product', productId],
  queryFn: () => getProduct(productId),
});

Stale Time and Refetching

Fresh data needs no new request. Stale data refreshes in the background when the screen mounts or the app regains focus. Set staleTime to control the freshness window.

useQuery({
  queryKey: ['products'],
  queryFn: getProducts,
  staleTime: 1000 * 60 * 5,   // 5 minutes
  retry: 2,
});

Change Data with useMutation

Queries read data. Mutations send changes such as creating, updating, or deleting. After a successful change, invalidate the related key so the list reloads.

import { useMutation, useQueryClient } from '@tanstack/react-query';

function AddProduct() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: createProduct,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
  });

  return (
    <Button
      title={mutation.isPending ? 'Saving...' : 'Add Product'}
      onPress={() => mutation.mutate({ title: 'New item', price: 20 })}
    />
  );
}

Pull to Refresh

const { data, refetch, isRefetching } = useQuery({ queryKey: ['products'], queryFn: getProducts });

<FlatList
  data={data}
  refreshing={isRefetching}
  onRefresh={refetch}
  renderItem={renderProduct}
/>

Infinite Scrolling

useInfiniteQuery loads one page at a time and appends results when the user nears the list end.

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ['feed'],
  queryFn: ({ pageParam = 1 }) => getFeed(pageParam),
  getNextPageParam: (last, all) => (last.length ? all.length + 1 : undefined),
});

<FlatList
  data={data?.pages.flat()}
  onEndReached={() => hasNextPage && fetchNextPage()}
  renderItem={renderPost}
/>

Benefits at a Glance

  • Automatic loading and error states
  • Shared cache across screens
  • Background refresh keeps data current
  • Retry on failure without extra code
  • Built-in pagination and refresh helpers

Summary

React Query treats server data as a managed cache. Wrap the app in a provider, fetch with useQuery, and send changes with useMutation. Use clear query keys, set sensible stale times, and invalidate keys after changes so every screen shows current data.