Лекция 7. Сетевые запросы и хранение данных на устройстве
Введение
Почти любое полезное мобильное приложение взаимодействует с внешним миром: загружает ленту новостей, отправляет форму на сервер, синхронизирует профиль пользователя. При этом мобильная сеть нестабильна (метро, лифт, роуминг), а пользователь ожидает, что приложение останется отзывчивым даже при потере соединения. Поэтому две темы идут рука об руку: как корректно ходить в сеть и как хранить данные на самом устройстве.
В этой лекции разберём:
- HTTP-запросы из приложения (
fetch/axios,async/await, разбор ответа, заголовки); - обработку ошибок сети: коды статусов, таймауты, базовые повторные попытки (retry);
- локальное хранение «ключ-значение» через AsyncStorage и безопасное хранение через SecureStore;
- реляционное хранение через SQLite, критерии выбора AsyncStorage vs SQLite;
- сериализацию JSON и вводное кеширование ответов локально.
Глубокое погружение в политики кеширования, offline-first и сложные retry-стратегии — в лекциях 12–13. Здесь — практический минимум, который нужен в каждом проекте.
1. HTTP-запросы из приложения
1.1. fetch — встроенный инструмент
В React Native (как и в браузере) доступен глобальный fetch. Он возвращает Promise,
поэтому удобно использовать async/await.
async function loadTodos() { const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5'); const data = await response.json(); // разбор тела ответа из JSON return data;}Важно понимать две стадии:
await fetch(...)— получаем объектResponse(статус, заголовки), но не тело.await response.json()(или.text()) — асинхронно читаем и разбираем тело.
1.2. Заголовки, методы, тело запроса
Для POST/PUT нужно указать метод, заголовки и сериализованное тело:
async function createTodo(todo, token) { const response = await fetch('https://api.example.com/todos', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, // передаём токен авторизации }, body: JSON.stringify(todo), // объект -> строка JSON }); return response.json();}Ключевые заголовки:
Content-Type: application/json— говорит серверу, в каком формате тело запроса;Accept— какой формат ответа мы ожидаем;Authorization— токен/ключ для аутентификации.
1.3. fetch vs axios
fetch встроен и не требует зависимостей, но у него есть особенности:
- он не считает ошибкой статусы 4xx/5xx (промис не отклоняется — нужно проверять вручную);
- нет встроенного таймаута;
- тело нужно вручную сериализовать/разбирать.
axios — популярная библиотека, которая решает это «из коробки»:
import axios from 'axios';
const api = axios.create({ baseURL: 'https://api.example.com', timeout: 10000, // таймаут в миллисекундах headers: { 'Content-Type': 'application/json' },});
const { data } = await api.get('/todos'); // тело уже разобрано в dataПреимущества axios: автоматический парсинг JSON, единый baseURL, перехватчики
(interceptors) для подстановки токена и логирования, встроенный таймаут, отклонение
промиса при HTTP-ошибках. Выбор между ними — вопрос масштаба проекта: для пары запросов
хватит fetch, для большого API удобнее axios.
2. Обработка ошибок сети
Сеть на телефоне нестабильна, поэтому ошибки — это норма, а не исключение. Их нужно обрабатывать осознанно.
2.1. Коды статусов и базовая обёртка
Напомним семейства HTTP-статусов:
- 2xx — успех (200 OK, 201 Created, 204 No Content);
- 4xx — ошибка клиента (400 Bad Request, 401 Unauthorized, 404 Not Found);
- 5xx — ошибка сервера (500, 502, 503).
Так как fetch не бросает исключение на 4xx/5xx, проверяем response.ok сами:
async function request(url, options = {}) { const response = await fetch(url, options); if (!response.ok) { // читаем текст ошибки, но не падаем, если тело пустое const message = await response.text().catch(() => 'Network error'); throw new Error(`${response.status}: ${message}`); } return response.json();}2.2. Таймауты
Без таймаута запрос может «висеть» бесконечно при плохой сети. С fetch таймаут
реализуется через AbortController:
async function requestWithTimeout(url, options = {}, timeoutMs = 8000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...options, signal: controller.signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } finally { clearTimeout(timer); // обязательно очищаем таймер }}В axios таймаут задаётся параметром timeout без дополнительного кода.
2.3. Повторные попытки (retry с backoff)
Временные ошибки (таймаут, 503, обрыв связи) часто исчезают при повторе. Простейшая стратегия — экспоненциальная задержка: каждый следующий повтор ждёт дольше.
async function withRetry(fn, { retries = 3, baseDelayMs = 300 } = {}) { let attempt = 0; while (true) { try { return await fn(); } catch (err) { if (attempt >= retries) throw err; // попытки исчерпаны const delay = baseDelayMs * Math.pow(2, attempt); // 300, 600, 1200 мс... await new Promise(r => setTimeout(r, delay)); attempt += 1; } }}
// const data = await withRetry(() => request('https://api.example.com/data'));Важные нюансы (подробно — в лекциях 12–13):
- повторять стоит только идемпотентные и временные ошибки (GET, 5xx, таймаут);
- не повторять 4xx-ошибки клиента (401, 404) — повтор не поможет;
- добавлять «джиттер» (случайный разброс), чтобы не перегружать сервер синхронными повторами.
3. Локальное хранение: AsyncStorage
3.1. Что это такое
AsyncStorage — простое асинхронное хранилище «ключ-значение», аналог localStorage
в вебе, но с асинхронным API. Хранит только строки. Подходит для небольших объёмов:
настройки пользователя, флаги, токен, последний открытый экран, кеш ответа.
npm install @react-native-async-storage/async-storage3.2. Основные операции
import AsyncStorage from '@react-native-async-storage/async-storage';
// Записьexport async function saveToken(token) { await AsyncStorage.setItem('token', token);}
// Чтение (вернёт строку или null, если ключа нет)export async function getToken() { return AsyncStorage.getItem('token');}
// Удалениеexport async function clearToken() { await AsyncStorage.removeItem('token');}Поскольку хранятся только строки, объекты нужно сериализовать:
export async function saveUser(user) { await AsyncStorage.setItem('user', JSON.stringify(user)); // объект -> строка}
export async function getUser() { const raw = await AsyncStorage.getItem('user'); return raw ? JSON.parse(raw) : null; // строка -> объект}Полезные методы: multiSet/multiGet (пакетные операции), getAllKeys, clear.
3.3. Рекомендации и безопасность
-
Не храните секреты в открытом виде. AsyncStorage не шифруется: токены доступа, пароли, ключи API в нём лежат как обычный текст, доступный при компрометации устройства.
-
Для чувствительных данных используйте expo-secure-store (Keychain на iOS, Keystore на Android):
import * as SecureStore from 'expo-secure-store';await SecureStore.setItemAsync('token', token); // хранится в защищённом хранилище ОСconst token = await SecureStore.getItemAsync('token'); -
Минимизируйте число ключей, используйте неймспейсы (
auth:token,cache:todos). -
Все операции асинхронны — не забывайте
awaitи обрабатывайте ошибки.
4. Реляционное хранение: SQLite
4.1. Когда AsyncStorage уже мало
Когда данных много и они связаны между собой, нужны выборки, сортировки, фильтры и
агрегаты — хранилище «ключ-значение» становится неудобным. Тогда используют встроенную
реляционную БД SQLite. В Expo для этого есть пакет expo-sqlite.
npx expo install expo-sqlite4.2. Набросок работы с SQLite
import * as SQLite from 'expo-sqlite';
// Открываем (или создаём) базуconst db = await SQLite.openDatabaseAsync('app.db');
// Создание таблицыexport async function initDb() { await db.execAsync(` CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY NOT NULL, title TEXT NOT NULL, done INTEGER DEFAULT 0 ); `);}
// Вставка с параметрами (защита от SQL-инъекций)export async function addTodo(title) { await db.runAsync('INSERT INTO todos (title, done) VALUES (?, ?);', [title, 0]);}
// Выборкаexport async function getTodos() { return db.getAllAsync('SELECT * FROM todos ORDER BY id DESC;');}Важно: значения подставляйте через параметры ?, а не конкатенацией строк —
это защищает от SQL-инъекций.
4.3. AsyncStorage vs SQLite
| Критерий | AsyncStorage | SQLite (expo-sqlite) |
|---|---|---|
| Модель данных | Ключ-значение (только строки) | Реляционная (таблицы, строки, столбцы) |
| Запросы | Нет; читаем по ключу | Полноценный SQL: WHERE, ORDER BY, JOIN, агрегаты |
| Объём данных | Небольшой (настройки, токен, мелкий кеш) | Средний/большой, тысячи записей |
| Связи между сущностями | Нет | Да (внешние ключи, JOIN) |
| Сложность API | Очень простой | Выше: схема, миграции, транзакции |
| Транзакции | Нет | Да |
| Типичные сценарии | Флаги, тема, последний экран, токен | Список задач, офлайн-каталог, история, заметки |
Критерии выбора: если нужно сохранить пару значений или небольшой объект — берите AsyncStorage (или SecureStore для секретов). Если данные структурированы, их много, и по ним нужны выборки/сортировки/связи — берите SQLite. Альтернативы для сложных случаев: WatermelonDB, Realm (выходят за рамки лекции).
5. Сериализация и вводное кеширование
5.1. JSON.stringify / JSON.parse
JSON — основной формат обмена с серверами и хранения структур на устройстве:
JSON.stringify(obj)— объект в строку (для отправки в теле запроса или записи в AsyncStorage);JSON.parse(str)— строка обратно в объект.
Помните об ограничениях: функции, undefined, Date сериализуются не «как есть»
(Date превращается в строку ISO, и при parse останется строкой). Всегда оборачивайте
JSON.parse в обработку ошибок — данные на диске могут оказаться повреждёнными.
5.2. Кеширование ответов локально (вводно)
Простейший кеш — сохранять последний успешный ответ в AsyncStorage и показывать его при старте, пока грузятся свежие данные:
const CACHE_KEY = 'cache:todos';
export async function getTodosCached() { try { const fresh = await request('https://api.example.com/todos'); await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(fresh)); // обновляем кеш return fresh; } catch (err) { // сеть недоступна — отдаём последнее сохранённое const raw = await AsyncStorage.getItem(CACHE_KEY); if (raw) return JSON.parse(raw); throw err; // кеша тоже нет — пробрасываем ошибку }}Это упрощённый паттерн. Зрелые подходы (Cache-First, Network-First, Stale-While-Revalidate, инвалидация по времени, фоновая синхронизация) и инструменты вроде TanStack Query разберём в лекциях 12–13.
Краткие итоги
fetchвстроен и работает сasync/await; ответ читается в два шага (Response, затем.json()).fetchне бросает ошибку на 4xx/5xx — проверяйтеresponse.okвручную; axios делает это сам и поддерживает таймаут/перехватчики.- Сеть нестабильна: добавляйте таймауты (через
AbortController) и повторы с экспоненциальной задержкой, но повторяйте только временные ошибки. - AsyncStorage — простое хранилище «ключ-значение» (только строки); объекты сериализуйте через JSON. Секреты в нём не храните — используйте SecureStore.
- SQLite нужен, когда данных много и они связаны: даёт SQL-запросы, сортировки, фильтры, транзакции. Выбор между ними определяется структурой и объёмом данных.
- JSON (
stringify/parse) — основа и для обмена с сервером, и для локального кеша последнего успешного ответа.
Вопросы для самопроверки
- В чём разница между AsyncStorage и SQLite? Когда использовать каждый подход?
- Опишите принципы работы с AsyncStorage. Какие есть рекомендации по безопасности?