Dia 2 — AsyncStorage: o armazenamento simples
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
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.
| Necessidade | Chave-valor | Banco |
|---|---|---|
| Guardar e ler um valor | setItem e getItem | INSERT e SELECT |
| Filtrar por uma condição | ler tudo e filtrar em JavaScript | WHERE dentro da consulta |
| Contar | .length depois de ler tudo | COUNT(*) |
| Ordenar | sort depois de ler tudo | ORDER BY |
| Ligar dois dados | unir objetos em JavaScript | JOIN |
| Crescer sem custo | a escrita reescreve tudo | a 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