Урок 09

Кеширование и стратегии

Кеш ускоряет приложение и экономит трафик.

Уровни кеша

  1. HTTP-кеш — заголовки Cache-Control, ETag
  2. TanStack Query — кеш в памяти
  3. Persist — кеш в AsyncStorage
  4. Изображения — expo-image кеширует
  5. Компоненты — memo, useMemo

staleTime и gcTime

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    staleTime: 5 * 60_000,    // 5 минут данные считаются свежими
    gcTime: 30 * 60_000,      // 30 минут в памяти после использования
});
  • staleTime — как долго данные «свежие»
  • gcTime — как долго хранить неиспользуемые данные

Стратегии

Fresh — данные всегда свежие:

staleTime: Infinity

Network-only — всегда из сети:

staleTime: 0,
cacheTime: 0,

Cache-first — сначала кеш, потом сеть:

staleTime: 5 * 60_000

Stale-while-revalidate — показать старое, обновить в фоне:

staleTime: 5 * 60_000,
refetchOnMount: 'always',

Persistent cache

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

<PersistQueryClientProvider
    client={queryClient}
    persistOptions={{
        persister,
        maxAge: 24 * 60 * 60 * 1000,
        dehydrateOptions: {
            shouldDehydrateQuery: (query) => query.state.status === 'success',
        },
    }}
>
    <App />
</PersistQueryClientProvider>

maxAge — как долго хранить в AsyncStorage.

Кеш изображений

expo-image:

import { Image } from 'expo-image';

<Image
    source={{ uri: url }}
    cachePolicy="memory-disk"  // 'none' | 'disk' | 'memory' | 'memory-disk'
    transition={200}
/>

Prefetch

Загрузить заранее:

queryClient.prefetchQuery({
    queryKey: ['post', id],
    queryFn: () => fetchPost(id),
});

В onPress перед навигацией — данные будут готовы к моменту открытия экрана.

Инвалидация

queryClient.invalidateQueries({ queryKey: ['posts'] });           // все с этим ключом
queryClient.invalidateQueries({ queryKey: ['posts', id] });       // конкретный
queryClient.invalidateQueries({ queryKey: ['posts'], exact: true }); // точное совпадение

Обновление конкретной записи

queryClient.setQueryData(['post', id], newData);

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

onMutate: async (updated) => {
    await queryClient.cancelQueries({ queryKey: ['post', updated.id] });
    const previous = queryClient.getQueryData(['post', updated.id]);
    queryClient.setQueryData(['post', updated.id], updated);
    return { previous };
},
onError: (_, __, context) => {
    queryClient.setQueryData(['post', context?.previous?.id], context?.previous);
},
onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['post'] });
},

Что кешировать

  • Списки (посты, пользователи)
  • Детали (профиль, товар)
  • Справочники (категории, города)
  • Изображения
  • Результаты поиска

Что не кешировать

  • Персональные данные при логауте
  • Токены
  • Платёжную информацию
  • Live-данные (курсы, чат)

Очистка при логауте

function logout() {
    queryClient.clear();
    AsyncStorage.clear();
    SecureStore.deleteItemAsync('token');
}

Итоги

  • staleTime и gcTime
  • Persist для кеша между запусками
  • expo-image для изображений
  • Prefetch для предзагрузки
  • invalidateQueries для обновления
  • Очистка при логауте