iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

React Query

TanStack Query (a.k.a. React Query) is the de-facto standard for data fetching in React Native. It handles caching, deduping, retries, background refetch, offline behaviour, and mutations — everything you’d otherwise reinvent badly with useEffect.

queries, mutations, offline, focus refetch

EXAMPLE
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';
import { AppState, AppStateStatus, Platform } from 'react-native';
import { focusManager, onlineManager } from '@tanstack/react-query';
import React, { useEffect } from 'react';

// 1) One client per app
const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            staleTime:  60_000,            // 1 min — never refetch within this window
            gcTime:     5 * 60_000,         // formerly cacheTime
            retry:      2,
            refetchOnReconnect: true,
        },
    },
});

// 2) Online / focus integration — RN doesn't have window.onFocus / navigator.onLine
onlineManager.setEventListener((setOnline) => {
    return NetInfo.addEventListener((state) => setOnline(!!state.isConnected));
});

focusManager.setEventListener((handleFocus) => {
    const sub = AppState.addEventListener('change', (status: AppStateStatus) => {
        if (Platform.OS !== 'web') handleFocus(status === 'active');
    });
    return () => sub.remove();
});

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

// 4) Basic query
function useTodos() {
    return useQuery({
        queryKey: ['todos'],
        queryFn:  () => fetch('https://api.example.com/todos').then((r) => r.json()),
    });
}

function TodoList() {
    const { data, isPending, isError, refetch, isFetching } = useTodos();

    if (isPending) return <ActivityIndicator />;
    if (isError)   return <Text>Failed to load. <Button title="Retry" onPress={() => refetch()} /></Text>;

    return (
        <FlatList
            data={data}
            keyExtractor={(t) => t.id}
            renderItem={({ item }) => <Text>{item.title}</Text>}
            refreshing={isFetching}
            onRefresh={refetch}
        />
    );
}

// 5) Parameterised query
function useTodo(id) {
    return useQuery({
        queryKey: ['todo', id],
        queryFn:  () => fetch(`https://api.example.com/todos/${id}`).then((r) => r.json()),
        enabled:  !!id,
    });
}

// 6) Mutations
function useToggleTodo() {
    const qc = useQueryClient();
    return useMutation({
        mutationFn: ({ id, done }: { id: string; done: boolean }) =>
            fetch(`https://api.example.com/todos/${id}`, {
                method:  'PATCH',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify({ done }),
            }).then((r) => r.json()),

        onSuccess: (_data, { id }) => {
            qc.invalidateQueries({ queryKey: ['todo', id] });
            qc.invalidateQueries({ queryKey: ['todos'] });
        },
    });
}

function TodoRow({ todo }) {
    const toggle = useToggleTodo();
    return (
        <Pressable onPress={() => toggle.mutate({ id: todo.id, done: !todo.done })}>
            <Text style={{ textDecorationLine: todo.done ? 'line-through' : 'none' }}>{todo.title}</Text>
        </Pressable>
    );
}

// 7) Optimistic updates
function useToggleTodoOptimistic() {
    const qc = useQueryClient();
    return useMutation({
        mutationFn: ({ id, done }) =>
            fetch(`https://api.example.com/todos/${id}`, {
                method:  'PATCH',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify({ done }),
            }),
        onMutate: async ({ id, done }) => {
            await qc.cancelQueries({ queryKey: ['todos'] });
            const previous = qc.getQueryData(['todos']);
            qc.setQueryData(['todos'], (rows) => rows.map((t) => t.id === id ? { ...t, done } : t));
            return { previous };
        },
        onError: (_e, _vars, ctx) => qc.setQueryData(['todos'], ctx.previous),
        onSettled: () => qc.invalidateQueries({ queryKey: ['todos'] }),
    });
}

// UI flips instantly; if the server rejects, the previous list rolls back.

// 8) Infinite scroll
import { useInfiniteQuery } from '@tanstack/react-query';

function useFeed() {
    return useInfiniteQuery({
        queryKey: ['feed'],
        queryFn:  ({ pageParam }) =>
            fetch(`https://api.example.com/feed?cursor=${pageParam ?? ''}`).then((r) => r.json()),
        initialPageParam: null,
        getNextPageParam: (lastPage) => lastPage.nextCursor,
    });
}

function Feed() {
    const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useFeed();
    const items = data?.pages.flatMap((p) => p.items) ?? [];

    return (
        <FlatList
            data={items}
            keyExtractor={(i) => i.id}
            renderItem={({ item }) => <PostCard post={item} />}
            onEndReached={() => hasNextPage && fetchNextPage()}
            onEndReachedThreshold={0.5}
            ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
        />
    );
}

// 9) Persisting the cache across launches
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';

const persister = createAsyncStoragePersister({ storage: AsyncStorage });
persistQueryClient({ queryClient, persister, maxAge: 1000 * 60 * 60 * 24 });

// On launch, queries hydrate from AsyncStorage → app shows last-known data immediately, then refetches in the background.

// 10) Suspense mode (React 18+)
function TodoTitle({ id }) {
    const { data } = useSuspenseQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) });
    return <Text>{data.title}</Text>;
}

// Wrap in <Suspense fallback={…}>; the loading state moves to a boundary instead of every component.

// 11) Query keys — design them like API paths
// ['todos']                            list
// ['todos', { filter: 'open' }]        filtered list
// ['todos', id]                         single doc
// ['users', uid, 'todos']                user's todos
// Stable keys = predictable caching. Use a 'query-key factory' for big apps.

// 12) Throw on error vs return error
// queryFn should throw on non-2xx. Otherwise React Query treats the response as success.
async function fetchTodos() {
    const r = await fetch('https://api.example.com/todos');
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    return r.json();
}

// 13) Cancellation
useQuery({
    queryKey: ['search', term],
    queryFn:  async ({ signal }) => {
        const r = await fetch(`/search?q=${term}`, { signal });
        return r.json();
    },
});
// When the query key changes (new search term), the previous request is aborted.

// 14) Background sync on app focus
// Already handled by focusManager above. New screens automatically refetch when they mount
// if data is older than staleTime.

// 15) Devtools
// In RN, use @tanstack/react-query-devtools-rn or expose state in a debug screen.
// For Expo + React Navigation, a 'devtools' drawer screen helps a lot.

// 16) Common bugs
// • Query keys not stable → infinite refetch loops (objects with new identity each render)
// • staleTime: 0 → every focus triggers a fetch; battery and bandwidth hit
// • Mutating data directly instead of invalidateQueries → cache shows stale data after server change
// • Optimistic update without onError rollback → stuck UI on failures
// • queryFn returns a non-throwing error response → cache 'success' with error body
// • useEffect-based loading + React Query in the same screen → competing fetches
// • Refresh control without isFetching guard → UI flickers between idle and pulling

Why it matters

TanStack Query in RN means hooking onlineManager to NetInfo and focusManager to AppState so reconnect-refetch and app-resume-refetch actually fire. Layer in persistQueryClient with AsyncStorage so the app boots with last-known data, optimistic mutations for snappy UI, and stable query keys so the cache works for you instead of against you.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
import { useQuery } from '@tanstack/react-query';
const { data, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/users').then(r => r.json())
});
Try it Yourself »

Discussion

Loading…