Практика 7. Практическая работа 7. Сетевые запросы и кеширование (AsyncStorage)
Цели работы
- Научиться выполнять HTTP-запросы из приложения через
fetchиasync/await. - Корректно отображать три состояния экрана: загрузка, ошибка, данные.
- Обрабатывать ошибки сети (
try/catch, проверкаresponse.ok) и давать пользователю возможность повторить запрос. - Кешировать ответ в AsyncStorage и реализовать вручную паттерн «показать кеш, затем обновить из сети» (stale-while-revalidate).
- Понять, почему секреты нужно хранить в SecureStore, а не в AsyncStorage.
Привязка к лекции 7 (разделы 1–3, 5).
Коротко о теории
fetchвозвращаетPromiseи читается в два шага: сначала объектResponse, затемawait response.json().fetchне бросает исключение на статусах 4xx/5xx — проверяйтеresponse.okвручную.- AsyncStorage хранит только строки — объекты сериализуйте через
JSON.stringify/JSON.parse. - AsyncStorage не шифруется — токены и ключи в нём хранить нельзя, для этого есть SecureStore.
Задание
Реализуйте экран, который загружает список постов с публичного API
(https://jsonplaceholder.typicode.com/posts или https://dummyjson.com/products),
показывает индикатор загрузки и сообщение об ошибке, кеширует данные локально.
Шаг 0. Подготовка проекта
Создайте проект (или используйте существующий) и установите AsyncStorage:
npx create-expo-app@latest practice07 --template blankcd practice07npx expo install @react-native-async-storage/async-storagenpm startШаг 1. Слой загрузки данных с проверкой ответа
Вынесите запрос в отдельную функцию. Проверяем response.ok, чтобы 4xx/5xx
превращались в исключение:
const API_URL = 'https://jsonplaceholder.typicode.com/posts?_limit=15';
async function fetchPosts() { const response = await fetch(API_URL); if (!response.ok) { // fetch не падает на 4xx/5xx — бросаем ошибку вручную throw new Error(`Ошибка сервера: ${response.status}`); } return response.json(); // массив объектов}Шаг 2. Кеш в AsyncStorage (get/set)
Объект нельзя записать напрямую — сериализуем его в JSON:
import AsyncStorage from '@react-native-async-storage/async-storage';
const CACHE_KEY = 'cache:posts';
async function saveCache(data) { await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(data)); // объект -> строка}
async function readCache() { const raw = await AsyncStorage.getItem(CACHE_KEY); // строка или null return raw ? JSON.parse(raw) : null; // строка -> объект}Шаг 3. Состояния экрана: loading / error / data
Заведите три состояния и загрузку в useEffect. Реализуем паттерн
stale-while-revalidate вручную: сначала показываем кеш, затем тянем свежее.
import { useEffect, useState, useCallback } from 'react';
export default function PostsScreen() { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
const load = useCallback(async () => { setError(null);
// 1. Сразу показываем кеш, если он есть (мгновенный отклик) const cached = await readCache(); if (cached) { setData(cached); setLoading(false); // данные уже есть, спиннер не нужен } else { setLoading(true); }
// 2. Затем обновляем из сети try { const fresh = await fetchPosts(); setData(fresh); await saveCache(fresh); // обновляем кеш } catch (err) { // если кеша не было — показываем ошибку; если был — оставляем кеш if (!cached) setError(err.message); } finally { setLoading(false); } }, []);
useEffect(() => { load(); }, [load]);
// ... рендер ниже}Шаг 4. Рендер: индикатор, ошибка, список, кнопка «Повторить»
import { View, Text, FlatList, ActivityIndicator, Button, StyleSheet } from 'react-native';
// внутри компонента, после load:if (loading) { return ( <View style={styles.center}> <ActivityIndicator size="large" /> <Text>Загрузка…</Text> </View> );}
if (error) { return ( <View style={styles.center}> <Text style={styles.error}>Не удалось загрузить данные: {error}</Text> <Button title="Повторить" onPress={load} /> </View> );}
return ( <FlatList data={data} keyExtractor={(item) => String(item.id)} renderItem={({ item }) => ( <View style={styles.row}> <Text style={styles.title}>{item.title}</Text> <Text numberOfLines={2}>{item.body}</Text> </View> )} />);
const styles = StyleSheet.create({ center: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 }, row: { padding: 12, borderBottomWidth: 1, borderBottomColor: '#eee' }, title: { fontWeight: 'bold', marginBottom: 4 }, error: { color: 'red', marginBottom: 12, textAlign: 'center' },});Шаг 5. Проверка работы
- Запустите при включённой сети — увидите список.
- Включите авиарежим и перезапустите экран — должны увидеть данные из кеша.
- Очистите кеш (временно вызовите
AsyncStorage.removeItem(CACHE_KEY)) и повторите при выключенной сети — увидите экран ошибки и работающую кнопку «Повторить».
Шаг 6 (обязательная заметка про безопасность)
В этой работе мы кешируем публичные данные — для них AsyncStorage подходит.
Но AsyncStorage не шифруется: всё лежит открытым текстом. Поэтому токены доступа,
пароли и ключи API храните в защищённом хранилище ОС через expo-secure-store:
import * as SecureStore from 'expo-secure-store';
await SecureStore.setItemAsync('token', token); // Keychain (iOS) / Keystore (Android)const token = await SecureStore.getItemAsync('token');Правило: публичный кеш — в AsyncStorage, секреты — в SecureStore.
Критерии оценки
- 25% — запрос выполняется через
fetch/async-await, данные рендерятся вFlatList. - 20% — корректно реализованы три состояния (loading / error / data).
- 20% — обработка ошибок:
try/catch, проверкаresponse.ok, рабочая кнопка «Повторить». - 25% — кеширование в AsyncStorage и логика «сначала кеш, затем сеть» (работает в авиарежиме).
- 10% — заметка/комментарий про SecureStore и аккуратный код (неймспейс ключа, очистка состояний).
Вопросы для самопроверки
- Почему
fetchне считает ошибкой статус 404 и как это обойти? - Зачем ответ читается в два шага (
fetch, затемresponse.json())? - Почему перед записью объекта в AsyncStorage нужен
JSON.stringify? - Что вернёт
getItem, если ключа в хранилище нет? - В чём идея паттерна stale-while-revalidate и какие у него плюсы для пользователя?
- Какие данные нельзя хранить в AsyncStorage и куда их класть вместо него?
Ресурсы
- AsyncStorage — https://react-native-async-storage.github.io/async-storage/
- Expo SecureStore — https://docs.expo.dev/versions/latest/sdk/securestore/
- MDN: Fetch API — https://developer.mozilla.org/ru/docs/Web/API/Fetch_API
- JSONPlaceholder — https://jsonplaceholder.typicode.com/
- DummyJSON — https://dummyjson.com/
- React Native FlatList — https://reactnative.dev/docs/flatlist