React Native course ยท Module 4: State and API data
React Query (TanStack Query) - Advanced Data Fetching
In this lesson7
TanStack Query is a powerful library for managing server state - caching, refreshing, synchronization, and more!
Installation
npx expo install @tanstack/react-queryConfiguration
// 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. What is required in useQuery?
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.