Armazenamento local em React Native: AsyncStorage, dados sensíveis e cache

Capítulo 11

Tempo estimado de leitura: 11 minutos

+ Exercício

Quando usar armazenamento local

Armazenamento local é a persistência de dados no dispositivo para que sobrevivam ao fechamento do app. Em React Native, uma escolha comum para dados simples é o AsyncStorage, que funciona como um “key-value store” assíncrono. Ele é adequado para preferências, flags, pequenos caches e metadados. Já dados sensíveis (tokens, senhas, chaves privadas) exigem mecanismos mais seguros, como Keychain (iOS) e Keystore (Android) via bibliotecas específicas.

Uma regra prática: AsyncStorage para conveniência e cache não sensível; Keychain/Keystore para segredos. Além disso, para evitar bugs e corrupção de dados, é importante padronizar serialização, versionar chaves, prever migrações e implementar invalidação de cache.

AsyncStorage na prática (preferências e cache simples)

Instalação

O AsyncStorage não vem mais no core do React Native. Use o pacote mantido pela comunidade:

npm i @react-native-async-storage/async-storage

Importação:

import AsyncStorage from '@react-native-async-storage/async-storage';

Boas práticas de modelagem: nomes, prefixos e versionamento

Evite chaves “soltas” e sem padrão. Use um prefixo do app e um namespace por domínio. Inclua versão para permitir migrações futuras.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • app:settings:v1
  • app:cache:feed:v1
  • app:cache:user:123:v1

Centralize as chaves em um único arquivo para reduzir erros de digitação e facilitar migrações:

export const STORAGE_KEYS = {  SETTINGS: 'myapp:settings:v1',  FEED_CACHE: 'myapp:cache:feed:v1',};

Serialização: sempre controle o formato

AsyncStorage armazena strings. Para objetos, serialize com JSON e defina um formato estável (com campos esperados e valores padrão). Evite armazenar estruturas muito grandes; prefira guardar apenas o necessário.

type Settings = {  theme: 'light' | 'dark';  notificationsEnabled: boolean;};const DEFAULT_SETTINGS: Settings = {  theme: 'light',  notificationsEnabled: true,};

Helpers robustos: leitura/escrita com fallback e validação

Falhas podem acontecer (dados corrompidos, JSON inválido, falta de espaço, interrupções). Crie funções utilitárias que tratem erros, retornem defaults e, quando necessário, limpem o valor inválido.

import AsyncStorage from '@react-native-async-storage/async-storage';async function safeSetJSON<T>(key: string, value: T): Promise<boolean> {  try {    const serialized = JSON.stringify(value);    await AsyncStorage.setItem(key, serialized);    return true;  } catch (err) {    // Logue em ferramenta de monitoramento se existir    return false;  }}async function safeGetJSON<T>(key: string, fallback: T): Promise<T> {  try {    const raw = await AsyncStorage.getItem(key);    if (raw == null) return fallback;    const parsed = JSON.parse(raw) as T;    return parsed ?? fallback;  } catch (err) {    // Se JSON estiver corrompido, remova para evitar loop de erro    try {      await AsyncStorage.removeItem(key);    } catch {}    return fallback;  }}

Quando o dado tem formato esperado, valide minimamente antes de usar. Uma validação simples pode evitar crashes:

function isSettings(value: any): value is Settings {  return (    value &&    (value.theme === 'light' || value.theme === 'dark') &&    typeof value.notificationsEnabled === 'boolean'  );}async function loadSettings(): Promise<Settings> {  const data = await safeGetJSON(STORAGE_KEYS.SETTINGS, DEFAULT_SETTINGS);  return isSettings(data) ? data : DEFAULT_SETTINGS;}

Passo a passo: persistindo preferências e sincronizando com estado global

O objetivo é: (1) carregar preferências ao iniciar, (2) refletir no estado global, (3) persistir toda vez que mudar, (4) evitar escrita excessiva.

1) Criar um “repositório” de Settings

import { STORAGE_KEYS } from './storageKeys';type Settings = {  theme: 'light' | 'dark';  notificationsEnabled: boolean;};const DEFAULT_SETTINGS: Settings = {  theme: 'light',  notificationsEnabled: true,};export async function getSettings(): Promise<Settings> {  return await safeGetJSON(STORAGE_KEYS.SETTINGS, DEFAULT_SETTINGS);}export async function setSettings(next: Settings): Promise<boolean> {  return await safeSetJSON(STORAGE_KEYS.SETTINGS, next);}

2) Carregar uma vez e manter em estado global

Independentemente da solução de estado global, o padrão é o mesmo: inicialize com defaults, carregue do storage e aplique. Abaixo um exemplo genérico com um “store” simplificado (adapte ao seu gerenciador):

type SettingsState = {  settings: Settings;  hydrated: boolean;  setTheme: (t: Settings['theme']) => void;  setNotificationsEnabled: (v: boolean) => void;  hydrate: () => Promise<void>;};
export const useSettingsStore = create<SettingsState>((set, get) => ({  settings: DEFAULT_SETTINGS,  hydrated: false,  setTheme: (theme) => set((s) => ({ settings: { ...s.settings, theme } })),  setNotificationsEnabled: (notificationsEnabled) =>    set((s) => ({ settings: { ...s.settings, notificationsEnabled } })),  hydrate: async () => {    const loaded = await getSettings();    set({ settings: loaded, hydrated: true });  },}));

Na inicialização do app (por exemplo, no componente raiz), chame hydrate e evite renderizar telas dependentes antes disso, se necessário.

3) Persistir mudanças com debounce (evitar muitas escritas)

Persistir a cada mudança pode gerar muitas operações. Use debounce (atraso) para agrupar alterações rápidas:

let persistTimer: any;export function schedulePersistSettings(next: Settings) {  if (persistTimer) clearTimeout(persistTimer);  persistTimer = setTimeout(() => {    setSettings(next);  }, 300);}

Integre isso no ponto onde o estado muda (middleware, subscribe do store, ou efeito no componente). Exemplo com subscribe:

const unsubscribe = useSettingsStore.subscribe(  (state) => state.settings,  (settings) => {    schedulePersistSettings(settings);  },  { equalityFn: (a, b) => JSON.stringify(a) === JSON.stringify(b) } // simples, ajuste conforme necessidade);

Se seu gerenciador não suporta subscribe com seletor, persista em um useEffect no componente que controla as preferências.

Cache local: TTL, invalidação e prevenção de dados obsoletos

Cache é útil para melhorar percepção de performance e reduzir chamadas de rede. Um cache simples deve ter: (1) payload, (2) timestamp, (3) versão, (4) política de expiração (TTL) e (5) estratégia de invalidação.

Modelo de cache com TTL

type CacheEntry<T> = {  v: number;          // versão do formato  savedAt: number;    // Date.now()  ttlMs: number;      // tempo de vida  data: T;};function isExpired(entry: CacheEntry<any>): boolean {  return Date.now() - entry.savedAt > entry.ttlMs;}

Salvar e ler cache com fallback

const FEED_CACHE_KEY = 'myapp:cache:feed:v1';async function setFeedCache(items: any[]) {  const entry: CacheEntry<any[]> = {    v: 1,    savedAt: Date.now(),    ttlMs: 5 * 60 * 1000, // 5 min    data: items,  };  await safeSetJSON(FEED_CACHE_KEY, entry);}async function getFeedCache(): Promise<any[] | null> {  const entry = await safeGetJSON<CacheEntry<any[]> | null>(FEED_CACHE_KEY, null);  if (!entry) return null;  if (entry.v !== 1) return null;  if (isExpired(entry)) return null;  return entry.data;}

Invalidação manual e por eventos

Além do TTL, invalide cache quando ocorrerem eventos que tornam os dados obsoletos:

  • Logout: remover caches e preferências específicas do usuário.
  • Troca de conta: separar chaves por usuário (userId) ou limpar ao alternar.
  • Atualização de app com mudança de formato: aumentar versão e migrar/limpar.
async function clearUserCaches(userId: string) {  const keys = [    `myapp:cache:feed:user:${userId}:v1`,    `myapp:cache:profile:user:${userId}:v1`,  ];  await AsyncStorage.multiRemove(keys);}

Estratégia “stale-while-revalidate” (prática para UX)

Uma abordagem comum: renderize imediatamente o cache (mesmo que esteja perto de expirar) e, em paralelo, busque dados novos. Se a requisição falhar, o usuário ainda vê algo.

async function loadFeed() {  const cached = await getFeedCache();  if (cached) {    // atualize UI com cached primeiro  }  try {    const fresh = await fetchFeedFromApi();    // atualize UI com fresh    await setFeedCache(fresh);  } catch (e) {    // mantenha cached se existir e trate erro (ex.: toast)  }}

Versionamento de chaves e migrações simples

Com o tempo, o formato do dado muda. Se você apenas sobrescrever, pode quebrar leitura de versões antigas. Existem dois padrões comuns:

  • Versionar na chave (ex.: settings:v1, settings:v2) e migrar na inicialização.
  • Versionar no payload (campo schemaVersion) e migrar ao ler.

Versionar na chave costuma ser mais simples para migrações pequenas: você lê a chave antiga, converte e grava na nova, então remove a antiga.

Exemplo: migrar settings:v1 para settings:v2

Suponha que em v2 você renomeou notificationsEnabled para pushEnabled.

const SETTINGS_V1 = 'myapp:settings:v1';const SETTINGS_V2 = 'myapp:settings:v2';type SettingsV1 = {  theme: 'light' | 'dark';  notificationsEnabled: boolean;};type SettingsV2 = {  theme: 'light' | 'dark';  pushEnabled: boolean;};const DEFAULT_V2: SettingsV2 = { theme: 'light', pushEnabled: true };async function migrateSettingsIfNeeded(): Promise<void> {  const v2 = await AsyncStorage.getItem(SETTINGS_V2);  if (v2 != null) return;  const v1raw = await AsyncStorage.getItem(SETTINGS_V1);  if (v1raw == null) {    await AsyncStorage.setItem(SETTINGS_V2, JSON.stringify(DEFAULT_V2));    return;  }  try {    const v1 = JSON.parse(v1raw) as SettingsV1;    const next: SettingsV2 = {      theme: v1.theme === 'dark' ? 'dark' : 'light',      pushEnabled: !!v1.notificationsEnabled,    };    await AsyncStorage.setItem(SETTINGS_V2, JSON.stringify(next));    await AsyncStorage.removeItem(SETTINGS_V1);  } catch {    // Se estiver corrompido, reseta para default e remove v1    await AsyncStorage.setItem(SETTINGS_V2, JSON.stringify(DEFAULT_V2));    await AsyncStorage.removeItem(SETTINGS_V1);  }}

Execute a migração no bootstrap do app, antes de hidratar o estado global.

Tratamento de falhas e prevenção de corrupção de dados

Problemas comuns

  • JSON inválido: app caiu no meio de uma escrita, ou houve escrita manual incorreta.
  • Condição de corrida: duas partes do app escrevem a mesma chave com valores diferentes.
  • Dados grandes: aumenta chance de falha e degrada performance.
  • Escritas frequentes: pode causar travamentos e aumentar risco de inconsistência.

Boas práticas objetivas

  • Escrita centralizada: um “repositório” por domínio (settings, cache, sessão) para evitar múltiplos lugares gravando a mesma chave.
  • Operações atômicas por domínio: prefira gravar um objeto inteiro de settings em vez de várias chaves separadas, reduzindo inconsistência.
  • Debounce/throttle para persistir alterações rápidas.
  • Validação e defaults ao ler; se inválido, remova e regrave.
  • multiGet/multiSet para reduzir round-trips quando lidar com várias chaves.
  • Separar por usuário quando o dado depende de conta.

Exemplo: leitura de múltiplas chaves com fallback

async function loadBootstrap() {  const pairs = await AsyncStorage.multiGet([    'myapp:settings:v2',    'myapp:cache:feed:v1',  ]);  const map = Object.fromEntries(pairs);  const settingsRaw = map['myapp:settings:v2'];  const feedRaw = map['myapp:cache:feed:v1'];  let settings = DEFAULT_V2;  try {    if (settingsRaw) settings = JSON.parse(settingsRaw);  } catch {    await AsyncStorage.removeItem('myapp:settings:v2');  }  let feed: any[] | null = null;  try {    if (feedRaw) {      const entry = JSON.parse(feedRaw) as CacheEntry<any[]>;      if (entry && entry.v === 1 && !isExpired(entry)) feed = entry.data;    }  } catch {    await AsyncStorage.removeItem('myapp:cache:feed:v1');  }  return { settings, feed };}

Dados sensíveis: por que AsyncStorage não é suficiente

AsyncStorage não foi projetado para segredos. Em geral, ele não oferece garantias de criptografia forte e pode ser alvo em dispositivos comprometidos, backups ou cenários de engenharia reversa. Para segredos (tokens de sessão, refresh tokens, chaves de API privadas, PIN), use armazenamento seguro baseado em Keychain (iOS) e Keystore (Android).

Alternativas comuns (via bibliotecas)

  • react-native-keychain: interface para Keychain/Keystore, adequada para credenciais e tokens.
  • expo-secure-store (em projetos Expo): armazenamento seguro com API simples.

O padrão recomendado é: token/segredo no secure storage e metadados não sensíveis no AsyncStorage (ex.: timestamp do último sync, flags, cache público).

Exemplo com Keychain/Keystore (conceitual)

Instale e use uma biblioteca de secure storage. Exemplo com react-native-keychain:

npm i react-native-keychain
import * as Keychain from 'react-native-keychain';export async function saveAuthToken(token: string) {  // Armazena como “senha” associada a um serviço/usuário  await Keychain.setGenericPassword('auth', token, { service: 'myapp.auth' });}export async function getAuthToken(): Promise<string | null> {  const creds = await Keychain.getGenericPassword({ service: 'myapp.auth' });  if (!creds) return null;  return creds.password;}export async function clearAuthToken() {  await Keychain.resetGenericPassword({ service: 'myapp.auth' });}

Boas práticas para segredos:

  • Rotacionar tokens quando possível (expiração curta + refresh).
  • Limpar no logout e ao detectar sessão inválida.
  • Evitar duplicar token em AsyncStorage, logs ou analytics.
  • Separar ambientes (dev/staging/prod) usando service diferente para não misturar credenciais.

Sincronização entre cache, preferências e estado global

Para evitar inconsistência, defina uma ordem clara:

  • Bootstrap: migrações → leitura do storage → hidratação do estado global.
  • Atualizações: estado global muda → persistência agendada (debounce) → confirmação opcional.
  • Logout/troca de conta: limpar secure storage → limpar caches por usuário → resetar estado global.

Fluxo de bootstrap sugerido (pseudo)

async function bootstrapApp() {  await migrateSettingsIfNeeded();  const settings = await safeGetJSON('myapp:settings:v2', DEFAULT_V2);  useSettingsStore.setState({ settings, hydrated: true });  const token = await getAuthToken();  useSessionStore.setState({ token, hydrated: true });}

Se o app depende do token para buscar dados, faça isso após hidratar sessão e settings, e use cache como fallback quando fizer sentido.

Checklist rápido de boas práticas

ObjetivoPrática
Evitar chaves desorganizadasPrefixo + namespace + versão (myapp:cache:feed:v1)
Evitar crashes por JSON inválidotry/catch + fallback + remover item corrompido
Reduzir escrita excessivaDebounce e persistência centralizada
Evitar cache obsoletoTTL + invalidação por eventos (logout, troca de conta)
Suportar evolução do appMigrações simples e versionamento
Proteger segredosKeychain/Keystore via biblioteca (não AsyncStorage)

Agora responda o exercício sobre o conteúdo:

Ao decidir onde persistir dados no dispositivo em um app React Native, qual abordagem melhor reduz riscos e mantém boas práticas para segredos e dados não sensíveis?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

AsyncStorage é adequado para preferências, flags e cache simples, mas não foi projetado para segredos. Tokens e credenciais devem ir para armazenamento seguro baseado em Keychain/Keystore, reduzindo exposição em cenários como backups ou dispositivos comprometidos.

Próximo capitúlo

Integração com recursos do dispositivo em React Native: câmera e localização via bibliotecas

Arrow Right Icon
Capa do Ebook gratuito React Native Essencial: criando apps completos com JavaScript e boas práticas
69%

React Native Essencial: criando apps completos com JavaScript e boas práticas

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.