Перейти к содержимому

Практика 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 blank
cd practice07
npx expo install @react-native-async-storage/async-storage
npm 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. Проверка работы

  1. Запустите при включённой сети — увидите список.
  2. Включите авиарежим и перезапустите экран — должны увидеть данные из кеша.
  3. Очистите кеш (временно вызовите 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 и аккуратный код (неймспейс ключа, очистка состояний).

Вопросы для самопроверки

  1. Почему fetch не считает ошибкой статус 404 и как это обойти?
  2. Зачем ответ читается в два шага (fetch, затем response.json())?
  3. Почему перед записью объекта в AsyncStorage нужен JSON.stringify?
  4. Что вернёт getItem, если ключа в хранилище нет?
  5. В чём идея паттерна stale-while-revalidate и какие у него плюсы для пользователя?
  6. Какие данные нельзя хранить в AsyncStorage и куда их класть вместо него?

Ресурсы