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.
