APIs e armazenamento no React Native com Expo
Busque uma API com fetch e salve dados locais com AsyncStorage no React Native, tratando carregamento, HTTP inválido e JSON corrompido.
No React Native, fetch busca dados pela rede e AsyncStorage guarda pequenos
valores persistentes no aparelho. A diferença central é simples: API conversa
com outro sistema; armazenamento local sobrevive a renderizações e reinícios
sem depender da rede. Nesta lição, você implementa os dois caminhos e trata
falhas observáveis.
O exemplo continua a lista criada em listas e navegação. Vamos buscar uma dica diária e salvar os hábitos. Para uma visão de contratos HTTP, consulte também o que é API REST.
Entrega pela rua e mantimentos na despensa
Imagine que uma refeição pode vir de um restaurante ou da sua despensa. Pedir ao restaurante depende de endereço, conexão, tempo e resposta do outro lado. Abrir a despensa é local e rápido, mas só encontra o que você guardou antes.
No mapa técnico, a entrega é uma requisição de rede com fetch; a despensa
é o armazenamento chave-valor do AsyncStorage. A resposta HTTP pode falhar,
demorar ou mudar de formato. O valor local pode não existir ou conter JSON de
uma versão antiga do app.
O limite é que AsyncStorage não é um cofre nem banco relacional. Ele mantém strings sem criptografia e sem consultas complexas. Tecnicamente, use-o para preferências e volumes pequenos; não guarde senha, token sensível ou grandes coleções apenas porque a API é conveniente.
Também não trate “local” como sinônimo de “sempre disponível”. O sistema pode limpar dados, uma gravação pode falhar e uma versão nova do app pode esperar outro formato. Dê à pessoa um estado seguro, registre o diagnóstico sem expor o conteúdo e planeje migração quando a estrutura mudar. Persistência é uma fronteira falível, mesmo sem cabo de rede envolvido.
Instale a versão compatível com o SDK
AsyncStorage saiu do núcleo do React Native há anos. Em um projeto Expo, instale o pacote da comunidade pela resolução do SDK:
npx expo install @react-native-async-storage/async-storageNo Expo SDK 57 testado, o comando selecionou AsyncStorage 2.2.0. Evite trocar
por npm install ...@latest sem conferir a compatibilidade: a linha principal
mais nova da biblioteca pode ter outra API e outro requisito nativo.
Confirme a instalação:
npx expo install --check
npm ls @react-native-async-storage/async-storageO ambiente de prova respondeu:
Encapsule a rede numa função que tenha contrato
Crie src/services/daily-tip.ts:
export type DailyTip = {
title: string;
completed: boolean;
};
export async function fetchDailyTip(
request: typeof fetch = fetch,
): Promise<DailyTip> {
const response = await request(
'https://jsonplaceholder.typicode.com/todos/1',
);
if (!response.ok) {
throw new Error(`A API respondeu com HTTP ${response.status}.`);
}
const data: unknown = await response.json();
if (!isDailyTip(data)) throw new Error('A API devolveu um formato inesperado.');
return data;
}response.ok cobre status entre 200 e 299. fetch não rejeita a Promise só
porque o servidor respondeu 404 ou 500; sem essa conferência, seu código pode
tentar tratar uma página de erro como dado válido.
O : unknown é intencional. Escrever const data: DailyTip não valida o JSON
que chegou. Crie a verificação usada acima:
function isDailyTip(value: unknown): value is DailyTip {
if (!value || typeof value !== 'object') return false;
const candidate = value as Record<string, unknown>;
return (
typeof candidate.title === 'string' &&
typeof candidate.completed === 'boolean'
);
}Esse é um type predicate. Em linguagem simples, a função olha o valor real e reúne provas para o TypeScript estreitar o tipo. O cast interno não confia no objeto; ele só permite inspecionar propriedades depois de confirmar que existe um objeto.
Observe uma resposta real antes de montar a interface
A URL usada no teste respondeu HTTP 200 em 22 de agosto de 2026:
node -e "fetch('https://jsonplaceholder.typicode.com/todos/1').then(async r => console.log(r.status, await r.json()))"O começo da saída real foi:
Serviço público de demonstração não deve ser dependência de produção. A saída é uma fotografia do teste, não uma promessa de disponibilidade futura.
Modele carregamento, sucesso e falha na tela
Três booleans soltas podem formar combinações impossíveis, como “carregando e com erro”. Para a prática curta, um union mantém um estado por vez:
type TipState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; tip: DailyTip }
| { status: 'error'; message: string };
const [tipState, setTipState] = useState<TipState>({ status: 'idle' });
async function loadTip() {
setTipState({ status: 'loading' });
try {
const tip = await fetchDailyTip();
setTipState({ status: 'success', tip });
} catch (error) {
setTipState({
status: 'error',
message: error instanceof Error ? error.message : 'Falha desconhecida.',
});
}
}Mostre ActivityIndicator em loading, a dica em success e texto com botão
de tentar novamente em error. A interface não deve ficar congelada esperando
uma Promise. Também pense em cancelamento ou em ignorar resposta atrasada caso a
tela saia antes do término.
Serialize antes de guardar e valide ao recuperar
AsyncStorage trabalha com strings. Crie src/lib/storage.ts:
import AsyncStorage from '@react-native-async-storage/async-storage';
import type { Habit } from '@/types/habit';
const HABITS_KEY = '@passo-a-passo/habits-v1';
export async function saveHabits(habits: Habit[]): Promise<void> {
await AsyncStorage.setItem(HABITS_KEY, JSON.stringify(habits));
}
export async function loadHabits(): Promise<Habit[]> {
const raw = await AsyncStorage.getItem(HABITS_KEY);
if (!raw) return [];
const value: unknown = JSON.parse(raw);
if (!Array.isArray(value)) throw new Error('Hábitos salvos são inválidos.');
return value as Habit[];
}O sufixo v1 na chave deixa espaço para uma migração futura. O cast final ainda
é uma simplificação didática: num produto, valide cada id, title e done,
porque JSON local também pode ficar antigo ou corrompido.
Integre carregamento e gravação com um efeito e uma trava de hidratação:
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
loadHabits()
.then((saved) => dispatch({ type: 'loaded', habits: saved }))
.finally(() => setHydrated(true));
}, []);
useEffect(() => {
if (!hydrated) return;
void saveHabits(habits);
}, [habits, hydrated]);Sem hydrated, o segundo efeito pode salvar a lista inicial vazia antes de a
leitura terminar e apagar dados existentes. Na analogia da despensa, seria
repor a prateleira com uma caixa vazia antes de olhar o que já havia nela.
Tecnicamente, a trava ordena dois efeitos assíncronos que compartilham a mesma
chave.
Reproduza falhas controladas
Injetar request permite simular HTTP 503 sem depender de derrubar um servidor:
test('expõe o status quando a API falha', async () => {
const request = jest.fn().mockResolvedValue({ ok: false, status: 503 });
await expect(fetchDailyTip(request as typeof fetch)).rejects.toThrow(
'A API respondeu com HTTP 503.',
);
});Para o armazenamento, devolva JSON quebrado no mock:
test('rejeita JSON local corrompido', async () => {
jest.spyOn(AsyncStorage, 'getItem').mockResolvedValue('{quebrado');
await expect(loadHabits()).rejects.toThrow(SyntaxError);
});Os dois testes executados produziram:
A classe visual de erro destaca que os cenários provocaram falhas, mas o Jest passou porque o comportamento esperado foi observado. Trate JSON inválido na tela com fallback e telemetria; não deixe uma exceção derrubar a experiência.
Missão: desenhe uma estratégia offline explícita
Faça a tela mostrar os hábitos locais imediatamente e uma dica remota quando a rede responder. Se a API falhar, preserve a lista e exiba “Dica indisponível; tente novamente”.
Verifique:
- modo avião não apaga hábitos;
- HTTP 503 não fica em loading infinito;
- JSON ausente vira [];
- JSON inválido vira fallback visível e registrável;
- nenhum token secreto é salvo no AsyncStorage.Você separou duas fronteiras com riscos diferentes: rede remota e armazenamento local. A partir daqui, a trilha entra nas ferramentas do Expo, começando pela escolha entre Expo Go e development builds.
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, Expo SDK 57.0.15, React Native 0.86.2, TypeScript 6.0.3, AsyncStorage 2.2.0 e Jest 29.7.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React Native 0.86 — Networking — reactnative.dev
- AsyncStorage — Usage — react-native-async-storage.github.io
- Expo — AsyncStorage — docs.expo.dev
- React — Synchronizing with Effects — react.dev


