Dia 1 — Por que o dado precisa sobreviver ao app fechar
O estado em memória e o que ele não resolve
O estado em memória e o que ele não resolve
useState guarda o dado em uma variável do JavaScript, e essa variável vive na memória do processo enquanto o app está aberto. É memória, não disco: quando o app fecha, o sistema mata o processo e o valor vai junto. Nada em useState diz "grave isto"; ele devolve o valor e uma função que troca o valor na próxima renderização. Toda a lista de tarefas do aluno fica em memória até o fim, e isso é a escolha certa para muita coisa — só não para tudo.
O sintoma do limite é o mais chato de um aplicativo: o usuário cria dez itens, passa o dia mexendo neles, fecha o aplicativo e abre de novo — o fechar o app é o caso mais curto do problema. A lista está vazia, sem aviso e sem erro no console. O bug não aparece em lugar nenhum e o diagnóstico quase sempre sai errado — "o useState não salvou". Salvou: guardou em memória, que é exatamente o que foi pedido a ele. O nome do problema é dado volátil, e a pergunta útil não é por que o estado sumiu, e sim onde este estado deveria estar.
É o que perder dado significa na prática: não é o aplicativo quebrar, e sim o aplicativo funcionar perfeitamente e abrir vazio amanhã. Esse detalhe é o que separa o bug de persistência de todo o resto.
O que a memória resolve e o que não resolve
Estado em memória é a resposta certa para dado recriável: se o usuário apagar a lista inteira e o app puxar de novo, a tela já tem o que precisa. Também é a resposta certa para dado derivado, aquele que sai de outro dado, porque recalcular é barato.
const [itens, setItens] = useState([]); const [filtro, setFiltro] = useState('todas'); const total = itens.reduce((soma, item) => soma + (item.feita ? 0 : item.valor), 0); const visiveis = filtro === 'todas' ? itens : itens.filter((item) => item.grupo === filtro);
O critério que separa os dois casos não é o tamanho nem a quantidade de itens: é se perder o dado é aceitável. Perder o filtro ativo é um incômodo de meio segundo; perder o que o usuário criou à mão é trabalho dele jogado fora, e o aplicativo que faz isso perde a confiança do usuário na primeira semana. A regra de bolso é uma pergunta sobre a origem: se a tela pode repuxar esse dado de algum lugar, ele pode ficar em memória; se ele nasceu do dedo do usuário e ninguém mais tem cópia, ele precisa sobreviver ao fechamento.
O lugar onde essa distinção aparece na prática é o botão de marcar como feita. A linha da tela tem feita em true porque o estado mudou, e no fim do dia esse true só existia naquela variável. Um usuário que marcou dez tarefas, fechou o aplicativo e abriu de novo para contar o que falta descobre que fez trabalho dobrado.
| Dado | Vale guardar em memória |
|---|---|
| Texto do campo que está sendo digitado | Sim, enquanto a tela estiver aberta |
| Filtro e ordem em uso na lista | Sim |
| Lista que vem do servidor e pode voltar | Sim |
| Lista que o usuário criou e quer que sobreviva | Não |
A tabela se lê como uma pergunta de produto, não como regra de sintaxe. O mesmo campo muda de coluna conforme quem o produziu: o texto digitado num formulário é estado de tela enquanto a pessoa digita, e vira dado persistido no instante em que o INSERT confirma.
A fronteira entre memória e disco
Persistência local é o nome do conjunto de técnicas que põem o dado fora da memória do processo — em arquivo, em armazenamento do sistema, em banco — para que ele sobreviver ao fechamento. A fronteira cabe em uma frase: memória morre com o processo, disco não. E o processo morre mais vezes do que o aluno imagina: fechar o aplicativo é o caso óbvio, mas reiniciar o celular, o sistema liberar memória por falta de bateria e a atualização do próprio sistema encerram o processo do mesmo jeito. Qualquer um dos três já apaga o que estava só na variável.
O useState continua existindo depois que a persistência entra, e passa a ter outro papel — a cópia em tela do que o banco tem. Essa troca de papel é a mudança conceitual do trimestre: antes o estado era a fonte da verdade, e depois ele passa a ser uma cópia que precisa ser recarregada.
O padrão que o aplicativo inteiro segue a partir daí é: salvar no banco, reler do banco, colocar no estado. Não é "gravar e manter em memória", porque estado que só existe na tela e banco que só existe depois de recarregar são duas verdades que divergem. Reler custa dois milissegundos e elimina a classe de bug em que a tela mostra uma coisa e o banco tem outra.
O exemplo desta página mostra o ciclo inteiro em quatro linhas de impressão: a variável recebe três tarefas, a tela monta um View com um Text por tarefa pendente, e então a variável é criada de novo, vazia. Isso é reabrir o app: mesmo código, mesmo arquivo, lista zerada. A partir daqui, o nome armazenamento local designa o lugar em que o dado vai para que essa última linha imprima três tarefas em vez de zero.
Exemplo
// Estado em memória: o dado vive na variável enquanto o processo existe. // O exemplo grava uma lista, mostra a tela, "fecha o app" e reabre. const { View, Text } = require('react-native'); let tarefas = []; function adicionar(titulo) { tarefas.push({ id: tarefas.length + 1, titulo: titulo, feita: false }); } function pendentes() { return tarefas.filter((tarefa) => !tarefa.feita); } adicionar('Revisar o WHERE'); adicionar('Ler o capitulo 4'); adicionar('Enviar o relatorio'); console.log('com o app aberto:', tarefas.length, 'tarefas'); // a arvore que a tela monta: um `View` por lista e um `Text` por tarefa const lista = ( <View> {pendentes().map((tarefa) => ( <Text key={tarefa.id}>{tarefa.titulo}</Text> ))} </View> ); console.log('a tela desenhou', lista.props.children.length, 'linhas'); // fechar o app e o fim do processo: a variavel nasce de novo, vazia tarefas = []; console.log('depois de reabrir:', tarefas.length, 'tarefas'); console.log('o que a tela mostraria agora:', tarefas);
Saída real
com o app aberto: 3 tarefas a tela desenhou 3 linhas depois de reabrir: 0 tarefas o que a tela mostraria agora: []
As três opções de onde guardar o dado
As três opções de onde guardar o dado
Três lugares resolvem a pergunta "onde o dado fica": a memória do processo, o armazenamento chave-valor do sistema e o banco relacional embarcado. Nenhum é o melhor em absoluto. A escolha vem do tipo de dado e, principalmente, da quantidade de perguntas que o aplicativo precisa fazer sobre ele.
Os dois primeiros locais têm nome de biblioteca: o chave-valor do React Native é o AsyncStorage, e o banco embarcado é o SQLite. O resto da página usa os dois nomes juntos — AsyncStorage para preferência e SQLite para coleção — porque é assim que o aplicativo real decide.
O armazenamento chave-valor é o mais simples do trio. setItem grava uma chave com uma string, getItem devolve a string, removeItem apaga a chave, clear apaga tudo. Quando o valor é objeto ou array, ele passa por JSON.stringify na entrada e JSON.parse na saída. O que ele não faz é filtrar, ordenar nem contar: se a tela quer só as tarefas concluídas, o aplicativo lê a lista inteira e filtra em JavaScript, todo acesso.
Os dois primeiros são local: o dado fica em um lugar dentro do aparelho, gerenciado pelo sistema, e só aquele aplicativo enxerga. O terceiro é o mesmo tipo de almacenamiento com estrutura de consulta por cima. A alternativa a todo trio seria guardar no remoto, num servidor acessado por rede, que traz a vantagem do dado compartilhado entre aparelhos e o custo de depender de conexão — e de um backend, que é assunto do ano seguinte.
| Opção | O que guarda | Como consulta | Relaciona dados | Volume |
|---|---|---|---|---|
| Memória | objeto e array | em JavaScript | sim, na mão | enquanto o app estiver aberto |
| Chave-valor | uma string por chave | só depois de ler tudo | não | poucos registros |
| Banco embarcado | tabela com colunas | SELECT com WHERE | chave estrangeira e JOIN | milhares de linhas |
O exemplo desta página põe as três para responder as mesmas perguntas e imprime o custo de cada uma: o par chave e valor devolve quatro linhas inteiras para filtrar o que eram três pendentes, e o banco devolve três linhas depois de examinar quatro. O número final é o que interessa — é a diferença entre uma comparar opções no papel e uma quantificação do custo real.
O que cada uma resolve
A memória resolve estado de tela: o texto do campo, a posição da rolagem, o filtro ativo, se a lista pode ser recriada. O chave-valor resolve preferência: tema escuro, última ordenação escolhida, identificador do usuário. O banco resolve coleção: tudo que o usuário cria, edita e apaga, e que precisa continuar lá depois do fechamento.
A segunda diferença que decide é quando o aplicativo precisa contar, somar ou relacionar. Um contador de pendências guardado em chave-valor significa JSON.parse da lista inteira a cada leitura; no banco a mesma pergunta é SELECT COUNT(*) FROM tarefas WHERE feita = 0, respondida sem trazer uma linha sequer para o JavaScript.
O volume separa os dois primeiros por uma linha muito concreta: chave-valor serve para dado pequeno — tema, último id, uma preferência. Ele foi feito para guardar pouquíssimas chaves, e cada gravação reescreve a string inteira. Collection grande não é o problema dele.
O terceiro critério é dado relacionado: quando uma tarefa aponta para uma categoria e o filtro é por nome de categoria, a pergunta já atravessa duas coleções. Chave-valor não tem como responder isso sem trazer as duas listas inteiras para a memória e unir em JavaScript — o que funciona, e fica mais lento a cada linha adicionada.
A escolha se faz pela pergunta
O SQL aparece no momento em que a pergunta muda de forma. Enquanto a pergunta é "qual é o valor da chave tema?", chave-valor é mais curto e mais rápido. Quando a pergunta passa a ser "quais tarefas estão vencidas e pertencem à categoria trabalho?", ela ganha estrutura — três entidades, uma condição composta, uma ordem — e o banco responde isso sem devolver o resto.
É por isso que a decisão se toma pela pergunta que o aplicativo faz ao dado, e não por preferência do time. Guardar uma lista de tarefas em chave-valor funciona até a primeira pergunta composta; a partir daí, cada resposta custa ler, converter e filtrar a lista inteira.
A única decisão ruim aqui é a do meio: começar com banco para tudo. Ela traz migration, PRAGMA e abrir e migrar o arquivo antes da primeira tela aparecer, para guardar uma preferência que cabia em duas linhas. A ordem prática é a do trimestre: armazenamento local simples para preferência, banco para coleção.
A resposta de uma linha para a dúvida de sempre — "eu escolho o banco" ou "eu escolho chave-valor" — é: escreva a pergunta que a tela faz ao dado. Se a resposta precisa atravessar duas coleções, ou contar, ou filtrar por condição composta, é banco. Se é o valor de uma chave, é escolher armazenamento no simples.
Exemplo
// As tres opcoes de armazenamento respondendo as mesmas tres // perguntas: guardar, consultar por condicao e relacionar. O exemplo conta // quantos registros cada opcao precisou tocar para responder. const tarefas = [ { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0, categoria_id: 1 }, { id: 2, titulo: 'Ler o capitulo 4', prazo: '2026-09-12', feita: 1, categoria_id: 1 }, { id: 3, titulo: 'Enviar o relatorio', prazo: '2026-09-08', feita: 0, categoria_id: 2 }, { id: 4, titulo: 'Comprar cafe', prazo: '2026-09-07', feita: 0, categoria_id: 2 }, ]; const categorias = [ { id: 1, nome: 'estudo' }, { id: 2, nome: 'trabalho' }, ]; // 1. memoria: o objeto existe enquanto o processo existir const memoria = { tarefas: tarefas.slice() }; console.log('memoria -> guarda', Object.keys(memoria).length, 'colecao'); // 2. chave-valor: uma string por chave. Toda consulta passa por JSON.parse const chaveValor = new Map(); chaveValor.set('tarefas', JSON.stringify(tarefas)); function consultarChaveValor(chave) { return JSON.parse(chaveValor.get(chave)); } console.log('chave-valor -> devolve', consultarChaveValor('tarefas').length, 'linhas inteiras'); // 3. banco: uma tabela por nome, e a consulta escolhe o que volta const banco = { tarefas: tarefas.slice(), categorias: categorias.slice() }; function consultarTabela(tabela, condicao) { let examinadas = 0; const linhas = []; for (const linha of banco[tabela]) { examinadas += 1; if (condicao(linha)) linhas.push(linha); } return { linhas: linhas, examinadas: examinadas }; } const pendentes = consultarTabela('tarefas', (linha) => linha.feita === 0); console.log('banco -> devolve', pendentes.linhas.length, 'linhas, examinou', pendentes.examinadas); // relacionar e o que so o banco faz sem trazer as duas tabelas inteiras const trabalho = consultarTabela('tarefas', (linha) => linha.categoria_id === 2).linhas; console.log('tarefas da categoria trabalho:', trabalho.map((t) => t.titulo));
Saída real
memoria -> guarda 1 colecao chave-valor -> devolve 4 linhas inteiras banco -> devolve 3 linhas, examinou 4 tarefas da categoria trabalho: [ 'Enviar o relatorio', 'Comprar cafe' ]