Мобильная сеть пропадает в метро, лифте и самолёте. Хорошее приложение показывает сохранённые данные, принимает действия без сети и отправляет их, когда связь вернётся. Вопросы на собесе: где хранить данные, как хранить секреты и как синхронизировать офлайн-изменения.
Где хранить
| Хранилище | Что это | Для чего |
|---|---|---|
| AsyncStorage | асинхронное ключ-значение, строки | небольшие настройки, флаги |
| MMKV | быстрое синхронное ключ-значение на C++ через JSI | настройки, кеш, состояние стора |
SQLite (expo-sqlite, op-sqlite) | реляционная база | большие объёмы, запросы, офлайн-данные |
| WatermelonDB, Realm | базы с реактивными запросами и синхронизацией | сложный офлайн-first |
Keychain / Keystore (expo-secure-store, react-native-keychain) | защищённое хранилище ОС | токены, пароли, ключи |
| Файловая система | файлы | изображения, документы, большие ответы |
AsyncStorage не шифрует данные и не подходит для больших объёмов: значения это строки, всё приходится сериализовать в JSON. MMKV работает синхронно и в разы быстрее, поэтому его часто используют как хранилище для персистентности Zustand или Redux. Синхронный доступ удобен для чтения настроек при старте, но писать в него мегабайты на каждое нажатие всё равно не стоит.
Секреты
Токены доступа и refresh-токены хранят только в Keychain (iOS) и Keystore (Android). AsyncStorage и MMKV без шифрования лежат файлом в песочнице приложения: на рутованном устройстве или из резервной копии их можно прочитать. Ключи сторонних сервисов с серверными правами в приложение не кладут вообще: всё, что в бинарнике, считается публичным.
Состояние сети
@react-native-community/netinfo сообщает тип подключения и isConnected/isInternetReachable. Но «есть Wi-Fi» не значит «есть интернет»: надёжный признак это успешный запрос. Поэтому офлайн-логика опирается на ошибки сети, а NetInfo служит подсказкой, когда пора повторить.
Офлайн-очередь
Пользователь без сети отметил задачу выполненной. Схема:
- Сразу применить изменение локально (оптимистично) и показать его.
- Положить операцию в постоянную очередь (SQLite или MMKV), чтобы она пережила перезапуск.
- Когда сеть есть, отправлять операции по порядку. Успех: удалить из очереди. Сетевая ошибка: остановиться и повторить позже с экспоненциальной задержкой. Ошибка валидации (4xx): не повторять бесконечно, а пометить и показать пользователю.
- Каждой операции дать ключ идемпотентности (UUID), чтобы повтор после обрыва не создал дубль на сервере.
Конфликты
Пока устройство было офлайн, ту же запись поменяли на другом устройстве. Стратегии:
- Last write wins: побеждает изменение с более поздней меткой времени. Просто, но тихо теряет данные. Метки времени клиента ненадёжны, лучше версии или время сервера.
- Версии записей: сервер отклоняет изменение со старой версией (
409 Conflict), клиент получает свежие данные и решает: слить автоматически или спросить пользователя. - Слияние по полям: если изменены разные поля, их можно объединить.
- CRDT: структуры, которые сливаются без конфликтов, для совместного редактирования.
Кеш серверных данных
TanStack Query с персистером (через MMKV или AsyncStorage) сохраняет кеш на диск: после перезапуска приложение сразу показывает последние данные и тихо обновляет их. Для больших наборов данных и поиска нужна полноценная база.
Частые ошибки
- Токены в AsyncStorage.
- Огромный JSON в одном ключе AsyncStorage, который читается целиком на каждый чих.
- Очередь в памяти: операции теряются при закрытии приложения.
- Повтор без идемпотентности: двойные заказы.
- Доверять
isConnectedбольше, чем результату запроса.
Что спросят на собесе
- AsyncStorage, MMKV, SQLite: когда что.
- Где хранить токены и почему.
- Как спроектировать офлайн-режим для приложения задач.
- Как решать конфликты синхронизации.
- Зачем ключ идемпотентности.