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-storageImportaçã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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
app:settings:v1app:cache:feed:v1app: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-keychainimport * 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
servicediferente 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
| Objetivo | Prática |
|---|---|
| Evitar chaves desorganizadas | Prefixo + namespace + versão (myapp:cache:feed:v1) |
| Evitar crashes por JSON inválido | try/catch + fallback + remover item corrompido |
| Reduzir escrita excessiva | Debounce e persistência centralizada |
| Evitar cache obsoleto | TTL + invalidação por eventos (logout, troca de conta) |
| Suportar evolução do app | Migrações simples e versionamento |
| Proteger segredos | Keychain/Keystore via biblioteca (não AsyncStorage) |