Сеть, кэширование и хранение данных на устройстве
Практический блок: откуда берутся данные, где они кэшируются и что остаётся на устройстве после закрытия приложения.
Серверное состояние живёт по своим законам: оно устаревает, приходит с задержкой и может отсутствовать. Поэтому его не хранят как обычное состояние, а кэшируют.
Вопрос
«Как работаете с сетью? Зачем нужен TanStack Query, если есть fetch? Где храните токен и пользовательские настройки?»
Что на самом деле проверяет интервьюер
Понимаете ли вы специфику мобильного приложения: нестабильная сеть, дорогой трафик, выгрузка приложения из памяти, требования безопасности к токенам. В вебе многие из этих вопросов не возникают.
Развёрнутый ответ
Сеть: fetch, axios и что действительно важно
fetch доступен из коробки, axios добавляет перехватчики и удобную отмену. Гораздо важнее уметь назвать сопутствующие вещи: таймауты, повторные попытки, обновление токена, обработка отсутствия сети.
async function apiFetch(path: string, init: RequestInit = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // без таймаута запрос может висеть минутами
try {
const res = await fetch(BASE_URL + path, {
...init,
signal: controller.signal,
headers: { 'Content-Type': 'application/json', ...init.headers },
});
if (res.status === 401) return refreshTokenAndRetry(path, init);
if (!res.ok) throw new ApiError(res.status, await res.text());
return res.json();
} finally {
clearTimeout(timeout);
}
}
Почему ручной fetch в useEffect не масштабируется
На каждый экран приходится писать одно и то же: состояния loading и error, отмену устаревших запросов, обновление после возврата, повторный запрос при ошибке. Плюс два экрана, которым нужны одни и те же данные, сходят за двумя запросами. Библиотека кэша решает это разом:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ProfileScreen({ userId }) {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: () => apiFetch(`/users/${userId}`),
staleTime: 60_000, // минуту считаем данные свежими и не ходим в сеть
});
const queryClient = useQueryClient();
const rename = useMutation({
mutationFn: (name) => apiFetch(`/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify({ name }),
}),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['user', userId] }),
});
if (isLoading) return <Spinner />;
if (error) return <ErrorView onRetry={refetch} />;
return <Profile user={data} onRename={rename.mutate} />;
}
Ключевые понятия, которые стоит произнести: queryKey как идентификатор кэша, staleTime и устаревание, инвалидация после мутации, дедупликация одинаковых параллельных запросов, оптимистичное обновление.
Хранение на устройстве
| Что | Чем | Особенности |
| Настройки, флаги, черновики | AsyncStorage | асинхронный, не шифрует, простой key-value |
| То же, но быстро | MMKV | синхронный через JSI, умеет шифровать |
| Токены, пароли, ключи | Keychain (iOS) и Keystore (Android) | системное защищённое хранилище, доступ по биометрии |
| Структурированные данные, оффлайн | SQLite, WatermelonDB, Realm | запросы, индексы, синхронизация |
| Файлы и картинки | файловая система приложения | система может чистить каталог кэша |
// MMKV: синхронное чтение — можно инициализировать состояние без «мигания» экрана
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV({ id: 'app', encryptionKey: 'secret-from-keychain' });
storage.set('onboardingDone', true);
const done = storage.getBoolean('onboardingDone'); // никаких промисов
Оффлайн и нестабильная сеть
- Отслеживание соединения —
@react-native-community/netinfo: показать баннер, отложить запросы. - Персистентный кэш запросов, чтобы при запуске без сети показать последние данные, а не пустой экран.
- Очередь мутаций: действия пользователя копятся и отправляются, когда сеть вернётся.
- Идемпотентность на бэкенде — иначе повторная отправка создаст дубль заказа.
Типичные ошибки кандидатов
- Хранят токен в AsyncStorage и не понимают, чем это отличается от Keychain.
- Не ставят таймауты: в метро запрос висит, спиннер крутится вечно.
- Кладут в AsyncStorage мегабайты JSON и удивляются задержкам при старте.
- Не различают клиентское и серверное состояние — тащат ответы API в глобальный store и вручную пишут инвалидацию.
Как ответить кратко
«Сеть — fetch или axios с обязательным таймаутом через AbortController, перехватом 401 и обновлением токена. Поверх — TanStack Query или RTK Query: они дают кэш по ключу, дедупликацию, статусы, инвалидацию после мутаций и повторные попытки, поэтому руками я это не пишу. На устройстве: обычные настройки — AsyncStorage или, если важна скорость и синхронное чтение, MMKV на JSI; токены только в Keychain и Keystore; структурированные данные и оффлайн — SQLite, WatermelonDB или Realm. Отдельно продумываю оффлайн: netinfo, персистентный кэш и очередь отложенных мутаций.»