Урок 02

TanStack Query в React Native

TanStack Query работает в React Native так же, как в вебе. Но есть нюансы.

Установка

npm install @tanstack/react-query

Провайдер

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

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            staleTime: 60_000,
            gcTime: 5 * 60_000,
            retry: 2,
        },
    },
});

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

Refetch on focus

В мобильном приложении важно обновлять данные при возврате на экран:

import { focusManager } from '@tanstack/react-query';
import { AppState, Platform } from 'react-native';
import { useEffect } from 'react';

function onAppStateChange(status: AppStateStatus) {
    if (Platform.OS !== 'web') {
        focusManager.setFocused(status === 'active');
    }
}

useEffect(() => {
    const sub = AppState.addEventListener('change', onAppStateChange);
    return () => sub.remove();
}, []);

Теперь при возврате в приложение данные обновляются автоматически.

Refetch on screen focus

При навигации между экранами:

npm install @react-navigation/native @tanstack/react-query
import { useFocusEffect } from '@react-navigation/native';
import { useQueryClient } from '@tanstack/react-query';

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

    useFocusEffect(
        useCallback(() => {
            queryClient.invalidateQueries({ queryKey: ['posts'] });
        }, [queryClient])
    );
}

useQuery

function Posts() {
    const { data, isLoading, error, refetch, isRefetching } = useQuery({
        queryKey: ['posts'],
        queryFn: () => api.get('/posts').then((r) => r.data),
    });

    if (isLoading) return <ActivityIndicator />;
    if (error) return <ErrorState onRetry={refetch} />;

    return (
        <FlatList
            data={data}
            renderItem={({ item }) => <PostCard post={item} />}
            refreshing={isRefetching}
            onRefresh={refetch}
        />
    );
}

refreshing + onRefresh — pull to refresh.

useMutation

const queryClient = useQueryClient();

const mutation = useMutation({
    mutationFn: (data: PostInput) => api.post('/posts', data),
    onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ['posts'] });
        navigation.goBack();
    },
});

Оптимистичные обновления

useMutation({
    mutationFn: likePost,
    onMutate: async (postId) => {
        await queryClient.cancelQueries({ queryKey: ['posts'] });
        const previous = queryClient.getQueryData(['posts']);

        queryClient.setQueryData(['posts'], (old: Post[]) =>
            old.map((p) => (p.id === postId ? { ...p, liked: !p.liked } : p))
        );

        return { previous };
    },
    onError: (_, __, context) => {
        queryClient.setQueryData(['posts'], context?.previous);
    },
    onSettled: () => {
        queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
});

Persist Query Client

Кеш сохраняется между запусками:

npm install @tanstack/query-async-storage-persister @tanstack/react-query-persist-client @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';

const persister = createAsyncStoragePersister({
    storage: AsyncStorage,
    key: 'react-query-cache',
});

<PersistQueryClientProvider
    client={queryClient}
    persistOptions={{ persister, maxAge: 24 * 60 * 60 * 1000 }}
>
    <App />
</PersistQueryClientProvider>

Данные сохраняются между запусками приложения.

Онлайн-статус

npm install @react-native-community/netinfo
import NetInfo from '@react-native-community/netinfo';
import { onlineManager } from '@tanstack/react-query';

onlineManager.setEventListener((setOnline) => {
    return NetInfo.addEventListener((state) => {
        setOnline(!!state.isConnected);
    });
});

Запросы не выполняются офлайн.

Итоги

  • TanStack Query работает
  • focusManager для refetch при возврате
  • useFocusEffect для refetch на экране
  • Persist для кеша между запусками
  • onlineManager для офлайн-режима
  • Pull to refresh через refreshing