Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori4 min de leitura

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:

bash
npx expo install @react-native-async-storage/async-storage

No 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:

bash
npx expo install --check
npm ls @react-native-async-storage/async-storage

O ambiente de prova respondeu:

Dependencies are up to date └── @react-native-async-storage/async-storage@2.2.0

Encapsule a rede numa função que tenha contrato

Crie src/services/daily-tip.ts:

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:

ts
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:

bash
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:

200 { userId: 1, id: 1, title: 'delectus aut autem', completed: false }

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:

tsx
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:

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:

tsx
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:

ts
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:

ts
test('rejeita JSON local corrompido', async () => {
  jest.spyOn(AsyncStorage, 'getItem').mockResolvedValue('{quebrado');
  await expect(loadHabits()).rejects.toThrow(SyntaxError);
});

Os dois testes executados produziram:

PASS __tests__/daily-tip-test.ts PASS __tests__/storage-test.ts Tests: 2 passed, 2 total

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”.

text
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.

  • api react native
  • asyncstorage
  • fetch react native
  • armazenamento local
  • expo

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. React Native 0.86 — Networking — reactnative.dev
  2. AsyncStorage — Usage — react-native-async-storage.github.io
  3. Expo — AsyncStorage — docs.expo.dev
  4. React — Synchronizing with Effects — react.dev

Continue por aqui