React Native course ยท Module 4: State and API data

React Query (TanStack Query) - Advanced Data Fetching

3 min read ยท
In this lesson7

TanStack Query is a powerful library for managing server state - caching, refreshing, synchronization, and more!

Installation

npx expo install @tanstack/react-query

Configuration

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      gcTime: 10 * 60 * 1000, // 10 minutes (garbage collection)
      retry: 3,
      refetchOnWindowFocus: false, // disable on mobile
    },
  },
});

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

useQuery - Fetching Data

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

function CreatureList() {
  const {
    data: creatures,
    isLoading,
    isError,
    error,
    refetch,
    isRefetching,
  } = useQuery({
    queryKey: ['creatures'], // unique cache key
    queryFn: creatureService.getAll,
  });

  if (isLoading) {
    return <ActivityIndicator size="large" />;
  }

  if (isError) {
    return (
      <View>
        <Text>Error: {error.message}</Text>
        <Pressable onPress={() => refetch()}>
          <Text>Try again</Text>
        </Pressable>
      </View>
    );
  }

  return (
    <FlatList
      data={creatures}
      refreshing={isRefetching}
      onRefresh={refetch}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <Text>{item.name}</Text>
      )}
    />
  );
}

Parametric Query

function CreatureDetail({ creatureId }: { creatureId: string }) {
  const { data: creature, isLoading } = useQuery({
    queryKey: ['creature', creatureId], // key with parameter
    queryFn: () => creatureService.getById(creatureId),
    enabled: !!creatureId, // execute only when there is an ID
  });

  // ...
}

// Search with debounce
function SearchCreatures() {
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 300);

  const { data: results } = useQuery({
    queryKey: ['creatures', 'search', debouncedSearch],
    queryFn: () => creatureService.search(debouncedSearch),
    enabled: debouncedSearch.length > 2,
  });

  return (
    <View>
      <TextInput
        value={search}
        onChangeText={setSearch}
        placeholder="Search creatures..."
      />
      <FlatList data={results} /* ... */ />
    </View>
  );
}

useMutation - Modifying Data

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

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

  const createMutation = useMutation({
    mutationFn: creatureService.create,
    onSuccess: (newCreature) => {
      // Option 1: Invalidate - refetch
      queryClient.invalidateQueries({ queryKey: ['creatures'] });

      // Option 2: Optimistic cache update
      queryClient.setQueryData(['creatures'], (old: Creature[] | undefined) => {
        return old ? [...old, newCreature] : [newCreature];
      });
    },
    onError: (error) => {
      Alert.alert('Error', 'Failed to create creature');
    },
  });

  const handleSubmit = (data: Omit<Creature, 'id'>) => {
    createMutation.mutate(data);
  };

  return (
    <View>
      {/* Form */}
      <Pressable
        onPress={() => handleSubmit({ name: 'Triton', species: 'Triton', habitat: 'The Depths' })}
        disabled={createMutation.isPending}
      >
        <Text>{createMutation.isPending ? 'Creating...' : 'Create'}</Text>
      </Pressable>
    </View>
  );
}

Optimistic Updates

const deleteMutation = useMutation({
  mutationFn: creatureService.delete,
  onMutate: async (creatureId) => {
    // Cancel pending queries
    await queryClient.cancelQueries({ queryKey: ['creatures'] });

    // Snapshot the previous state
    const previousCreatures = queryClient.getQueryData(['creatures']);

    // Optimistically remove
    queryClient.setQueryData(['creatures'], (old: Creature[] | undefined) =>
      old?.filter((c) => c.id !== creatureId)
    );

    // Return context with previous state
    return { previousCreatures };
  },
  onError: (err, creatureId, context) => {
    // Restore previous state on error
    queryClient.setQueryData(['creatures'], context?.previousCreatures);
  },
  onSettled: () => {
    // Always refresh after completion
    queryClient.invalidateQueries({ queryKey: ['creatures'] });
  },
});

Infinite Query - Pagination

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

function InfiniteCreatureList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['creatures', 'infinite'],
    queryFn: ({ pageParam = 1 }) =>
      creatureService.getPaginated(pageParam, 20),
    getNextPageParam: (lastPage, pages) =>
      lastPage.hasMore ? pages.length + 1 : undefined,
    initialPageParam: 1,
  });

  const creatures = data?.pages.flatMap((page) => page.items) ?? [];

  return (
    <FlatList
      data={creatures}
      onEndReached={() => hasNextPage && fetchNextPage()}
      onEndReachedThreshold={0.5}
      ListFooterComponent={() =>
        isFetchingNextPage ? <ActivityIndicator /> : null
      }
      // ...
    />
  );
}

TanStack Query significantly simplifies server state management and automates caching, refreshing, and synchronization!

Code for this lesson: App.tsx
import React from 'react';
import { View, Text, Pressable, FlatList, ActivityIndicator, StyleSheet } from 'react-native';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

const queryClient = new QueryClient();

interface Creature {
  id: string;
  name: string;
}

// Simulated API request
async function fetchCreatures(): Promise<Creature[]> {
  await new Promise((resolve) => setTimeout(resolve, 800));
  return [
    { id: '1', name: 'Triton' },
    { id: '2', name: 'Mermaid' },
    { id: '3', name: 'Kraken' },
  ];
}

function CreatureList() {
  // TODO: use useQuery with queryKey: ['creatures'] and queryFn: fetchCreatures
  // Extract: data, isLoading, isError, refetch, isRefetching
  const {
    data: creatures,
    isLoading,
    isError,
    refetch,
    isRefetching,
  } = useQuery({
    queryKey: ['creatures'],
    queryFn: fetchCreatures,
  });

  // TODO: when isLoading === true, show <ActivityIndicator />
  if (isLoading) {
    return <ActivityIndicator size="large" color="#fff" style={styles.center} />;
  }

  // TODO: when isError === true, show an error message and a Try again button (onPress={refetch})
  if (isError) {
    return (
      <View style={styles.center}>
        <Text style={styles.error}>Error fetching data</Text>
        <Pressable style={styles.button} onPress={() => refetch()}>
          <Text style={styles.buttonText}>Try again</Text>
        </Pressable>
      </View>
    );
  }

  return (
    <FlatList
      style={styles.list}
      data={creatures}
      refreshing={isRefetching}
      onRefresh={refetch}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <Text style={styles.item}>{item.name}</Text>}
    />
  );
}

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

const styles = StyleSheet.create({
  list: { flex: 1, padding: 20, backgroundColor: '#1a5276' },
  center: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#1a5276',
  },
  item: { color: '#aed6f1', fontSize: 18, paddingVertical: 8 },
  error: { color: '#f1948a', fontSize: 16, marginBottom: 16 },
  button: {
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    backgroundColor: '#2980b9',
  },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is required in useQuery?

  2. 2. What does staleTime mean in React Query?

These are 2 of 4 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Click in order

    Click the useQuery elements in the correct order:

  • Horizontal ordering

    Arrange cache invalidation after a mutation:

  • Code editor

    Complete the code with loading, error and refetch handling.

Useful articles