Dia 2 — AsyncStorage: o armazenamento simples

Informatica · Conteudo · publicado em 05/10/2026
Dia 2 de 15

AsyncStorage: o armazenamento simples

Aula 1

Gravar e ler uma chave

Gravar e ler uma chave

O armazenamento chave-valor do React Native é um objeto com quatro métodos que o aplicativo inteiro usa: setItem(chave, valor), getItem(chave), removeItem(chave) e clear(). Os quatro devolvem promessa, e o await é obrigatório em todos — sem ele a gravação foi iniciada e o código já seguiu em frente.

O valor é sempre string. getItem devolve null quando a chave não existe, e não devolve undefined nem lança erro: a checagem é if (valor === null). Essa distinção importa porque null é o único jeito de o armazenamento dizer "não tenho essa chave" — um undefined apareceria na tela como texto renderizado, e o bug passaria despercebido numa lista.

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

await AsyncStorage.setItem('tema', 'escuro');
const tema = await AsyncStorage.getItem('tema');
if (tema === null) {
  // ainda nao gravou nada: usa o padrao do sistema
}

Como o valor é string, qualquer coisa mais rica passa por JSON.stringify na gravação e JSON.parse na leitura, nessa ordem, sempre. O par de conversão é o que dá ao armazenamento a aparência de aceitar objeto e array, e o exemplo desta página mostra o preço disso: uma lista de duas tarefas guardada como objeto ocupa uma string de 101 caracteres, e o que volta do getItem é typeof string, não array.

Prefixo nas chaves e o que ele evita

Nomear a chave com um prefixo — app:tema, app:ultimo_id — evita a colisão que aparece quando duas partes do aplicativo gravam no mesmo lugar com o mesmo nome. O prefixo também faz o aplicativo inteiro ser legível de uma vez, com um getAllKeys que devolve a lista do que existe.

O prefixo vale por uma razão prática: ele transforma o armazenamento em um espaço com nome. Sem ele, getAllKeys devolve ['tema', 'tarefas', 'login'] e nada diz de onde veio cada chave. Com ele, devolve ['app:tema', 'app:tarefas'] e dá para apagar o aplicativo inteiro em uma chamada filtrando pelo prefixo. É o mesmo cuidado que o prefixo de tabela dá no SQL, e resolve o mesmo problema: nome que não é global vira global por acidente.

mergeItem(chave, objeto) é o atalho para JSON.parse do valor, _merge_ do objeto recebido e setItem do resultado. Ele economiza uma linha no getItem e um parse do objeto antigo, e só funciona porque o valor guardado é um objeto serializado. Em uma chave que guarda string pura — o tema — ele não serve: não há objeto para mesclar.

Quando o chave-valor é a resposta certa

Ele serve para dado pequeno, de escrita rara e leitura frequente: tema, última tela visitada, token de acesso, data da última sincronização. O que caracteriza esse uso é a assimetria: ler é comum, gravar é raro. Um tema é lido toda vez que a tela monta e gravado uma vez a cada mudança de configuração.

O par serve para armazenar string pequena e ler de volta com custo desprezível, e é por isso que ele resolve preferência. Gravar reescreve o valor inteiro; ler devolve o valor inteiro. Enquanto o valor for pequeno, as duas são irrelevantes — é por isso que a lista de tarefas do exemplo aparece aqui apenas para mostrar a conversão, e não como recomendação de uso: ela é justamente o caso que a aula seguinte descarta.

O limite prático é a string: o armazenamento guarda megabytes no total, e gravar um objeto muito grande de uma vez é lento mesmo quando cabe. Se a pergunta que o aplicativo faz ao dado é "quais", o caminho já aponta para outro lugar.

O removeItem completa o conjunto e fecha o ciclo do exemplo: depois de apagar a chave, o getItem devolve null de novo — a mesma resposta da chave que nunca existiu. É por isso que o aplicativo trata "não existe" e "foi apagada" com o mesmo código: para ele, as duas situações são indistinguíveis, e devem ser.

Exemplo

// O armazenamento chave-valor guarda uma string por chave, entao um objeto
// so entra depois de `JSON.stringify`, e volta depois de `JSON.parse`.
const armazenamento = new Map();

function setItem(chave, valor) {
  return Promise.resolve(armazenamento.set(chave, String(valor)));
}

function getItem(chave) {
  const bruto = armazenamento.has(chave) ? armazenamento.get(chave) : null;
  return Promise.resolve(bruto);
}

function removeItem(chave) {
  armazenamento.delete(chave);
  return Promise.resolve();
}

function getAllKeys() {
  return Promise.resolve([...armazenamento.keys()]);
}

(async function () {
  await setItem('app:tema', 'escuro');

  const tema = await getItem('app:tema');
  console.log('tema lido:', tema, '- do tipo', typeof tema);

  const inexistente = await getItem('app:idioma');
  console.log('chave que nao existe devolve', inexistente);

  const tarefas = [
    { id: 1, titulo: 'Revisar o WHERE', feita: false },
    { id: 2, titulo: 'Ler o capitulo 4', feita: true },
  ];
  await setItem('app:tarefas', JSON.stringify(tarefas));

  const lido = await getItem('app:tarefas');
  console.log('o que foi guardado e uma string de', lido.length, 'caracteres');

  const deVolta = JSON.parse(lido);
  console.log('depois do parse:', deVolta.length, 'objetos');
  console.log('o primeiro titulo:', deVolta[0].titulo);

  console.log('chaves do armazenamento:', await getAllKeys());

  await removeItem('app:tema');
  console.log('depois do removeItem, o tema vale', await getItem('app:tema'));
})();

Saída real

tema lido: escuro - do tipo string
chave que nao existe devolve null
o que foi guardado e uma string de 101 caracteres
depois do parse: 2 objetos
o primeiro titulo: Revisar o WHERE
chaves do armazenamento: [ 'app:tema', 'app:tarefas' ]
depois do removeItem, o tema vale null
Aula 2

JSON no armazenamento e o limite do par chave-valor

JSON no armazenamento e o limite do par chave-valor

JSON.stringify transforma qualquer valor do JavaScript em uma string, e JSON.parse faz o caminho de volta. É esse par que dá ao armazenamento chave-valor a aparência de aceitar objeto, array, número e booleano. O que ele aceita de verdade é a string que o par produz — e a conversão tem consequências que o aluno só descobre depois.

O nome dos dois lados da conversão é o vocabulário do problema: serializar é escrever o objeto como texto, desserializar é ler esse texto de volta. O AsyncStorage só conhece a segunda metade; a primeira acontece no aplicativo, e por isso a falha de uma serialização mal pensada aparece como falha de leitura muito depois.

O que se perde na conversão: undefined e função simplesmente não existem no JSON e somem do objeto; Date vira a string do texto; Map, Set e classe viram objeto vazio ou objeto simples. Nada disso avisa: o parse não reclama e o bug aparece semanas depois, como "a data está undefined".

const registro = {
  id: 1,
  criadoEm: new Date('2026-09-05T10:00:00'),
  rotulo: 'primeira compra',
  etiquetas: ['novo', 'urgente'],
};

const texto = JSON.stringify(registro);
const deVolta = JSON.parse(texto);

console.log(typeof deVolta.criadoEm);   // string, nao Date
console.log(Array.isArray(deVolta.etiquetas));  // true

O detalhe da última linha é o que se aproveita: array sobrevive à conversão, porque JSON tem array. Por isso a lista de tarefas funciona no chave-valor sem que nada pareça errado — o Array.isArray devolve true, o .map funciona, e a tela desenha. A armadilha não é a lista quebrar; é a lista funcionar devagar e a data quebrar. Esse é o tipo de dado que o par resolve e o que ele não resolve, e a regra prática é: só grave no chave-valor o que o parse devolve igual.

O limite do par chave-valor

Toda pergunta sobre o dado exige o ciclo completo: ler a string inteira, converter, filtrar em JavaScript, descartar o resto. Não existe filtro no armazenamento — um getItem não recebe condição. Se a tela quer as tarefas pendentes, o aplicativo lê todas, converte todas e filtra todas, todo acesso.

NecessidadeChave-valorBanco
Guardar e ler um valorsetItem e getItemINSERT e SELECT
Filtrar por uma condiçãoler tudo e filtrar em JavaScriptWHERE dentro da consulta
Contar.length depois de ler tudoCOUNT(*)
Ordenarsort depois de ler tudoORDER BY
Ligar dois dadosunir objetos em JavaScriptJOIN
Crescer sem custoa escrita reescreve tudoa escrita toca uma linha

O desempenho piora de forma silenciosa. Com trinta registros nada incomoda; com muitos registros, cada leitura vira JSON.parse de uma string grande e o aplicativo engasga exatamente na hora de abrir a tela — que é a hora em que o usuário percebe.

O critério do meio da tabela é o que separa as duas colunas em qualquer código real: no chave-valor, consultar significa ler tudo e filtrar em memória, e o custo cresce com o tamanho da coleção e não com o tamanho da resposta. No banco, a mesma pergunta custa o mesmo com trinta linhas e com trinta mil. É por isso que a resposta à pergunta "guardar objeto direto" é sempre que não: não guardar objeto direto não é uma regra de estilo, é o momento em que o aplicativo escolhe onde a pergunta vai ser respondida.

A consequência prática para quem está começando é incomoda: o chave-valor aceita a lista, a lista aparece na tela, e o aplicativo passa nos testes. Ele só engasga com o usuário real, que tem duzentas tarefas em vez de cinco. Esse tipo de defeito — que cresce com o dado — é o que a tabela acima existe para antecipar.

A ponte para o banco

O dado guardado em chave-valor continua sendo um bom assunto para o banco quando a pergunta ganha forma: quando precisa de condição composta, de ordem, de contagem ou de vínculo com outra coleção. O JSON não desaparece — ele vira o formato de intercâmbio na escrita em lote, e a string JSON é o que entra no VALUES quando o aplicativo monta o INSERT a partir do que o formulário devolve.

E o limite do AsyncStorage é o que fecha o raciocínio da aula: ele é armazenamento de configuração, não de coleção. Guardar nele o que o usuário cria é possível e funciona até o dia em que o volume cresce — e quando isso acontece, o dado já está no formato errado para a pergunta que a tela está fazendo. A migração de chave-valor para banco é reescrever a mesma informação em outro lugar, não carregar o dado para outro aparelho.

Exemplo

// O valor guardado e sempre string: e por isso que o objeto passa por
// `JSON.stringify` na gravacao e por `JSON.parse` na leitura.
const armazenamento = new Map();

function setItem(chave, valor) {
  return Promise.resolve(armazenamento.set(chave, String(valor)));
}

function getItem(chave) {
  const bruto = armazenamento.has(chave) ? armazenamento.get(chave) : null;
  return Promise.resolve(bruto);
}

function removeItem(chave) {
  armazenamento.delete(chave);
  return Promise.resolve();
}

function getAllKeys() {
  return Promise.resolve([...armazenamento.keys()]);
}

(async function () {
  await setItem('app:tema', 'escuro');

  const tema = await getItem('app:tema');
  console.log('tema lido:', tema, '- do tipo', typeof tema);

  const inexistente = await getItem('app:idioma');
  console.log('chave que nao existe devolve', inexistente);

  // objeto e array: entram como string e voltam como objeto
  const tarefas = [
    { id: 1, titulo: 'Revisar o WHERE', feita: false },
    { id: 2, titulo: 'Ler o capitulo 4', feita: true },
  ];
  await setItem('app:tarefas', JSON.stringify(tarefas));

  const lido = await getItem('app:tarefas');
  console.log('o que foi guardado e uma string de', lido.length, 'caracteres');

  const deVolta = JSON.parse(lido);
  console.log('depois do parse:', deVolta.length, 'objetos');
  console.log('a chave continua sendo a mesma:', JSON.parse(lido)[0].titulo);

  console.log('chaves do armazenamento:', await getAllKeys());

  await removeItem('app:tema');
  console.log('depois do removeItem, o tema vale', await getItem('app:tema'));
})();

Saída real

tema lido: escuro - do tipo string
chave que nao existe devolve null
o que foi guardado e uma string de 101 caracteres
depois do parse: 2 objetos
a chave continua sendo a mesma: Revisar o WHERE
chaves do armazenamento: [ 'app:tema', 'app:tarefas' ]
depois do removeItem, o tema vale null