Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

localStorage: salvar dados no navegador com JS

localStorage guarda só string, é síncrono e devolve null em chave inexistente — com JSON.stringify, o limite de 5MB e o erro de salvar objeto sem converter.

Rodolfo Mori6 min de leitura

localStorage guarda pares chave-valor no navegador do usuário, presos ao domínio do site, e eles sobrevivem a fechar a aba e desligar o computador. A API inteira são cinco métodos:

js
localStorage.setItem('cliente', 'Ana Souza');
console.log(localStorage.getItem('cliente'));
Ana Souza

Duas características decidem como você vai usar isso na prática: só existe string lá dentro — objeto precisa de JSON.stringify na ida e JSON.parse na volta — e tudo é síncrono, o que significa que uma leitura grande trava a tela enquanto acontece. O resto desta lição é o que essas duas coisas provocam num carrinho de compras.

O localStorage é um armazenamento síncrono de strings por origem. Em palavras simples, cada domínio ganha uma gaveta de texto, e toda leitura ou escrita ocupa a thread da página até terminar.

A gaveta do domínio só aceita etiquetas de texto

Imagine um arquivo de fichas em que cada gaveta pertence a uma loja e cada registro aceita apenas uma etiqueta escrita. Para guardar uma ficha complexa, você precisa transformá-la em texto; para usá-la de novo, precisa reconstruir os campos. JSON.stringify e JSON.parse fazem essa passagem, mas não mudam a API do localStorage: ali dentro continua existindo string.

Abra a área Application do DevTools antes do primeiro exemplo. Para cada setItem, preveja chave, valor visível e tipo devolvido por getItem; depois confira os três. Recarregue a página e repita a leitura para verificar, em vez de apenas assumir, a persistência.

Você pode passar qualquer valor para setItem. Ele converte para string antes de guardar, sem avisar:

js
localStorage.setItem('total', 199.9);
localStorage.setItem('freteGratis', true);
localStorage.setItem('cupom', null);

const total = localStorage.getItem('total');
const frete = localStorage.getItem('freteGratis');

console.log(total, typeof total);
console.log(frete, typeof frete);
console.log(localStorage.getItem('cupom'), typeof localStorage.getItem('cupom'));
199.9 string true string null string

O terceiro caso é o mais cruel: null virou a string "null", com quatro caracteres, que é um valor perfeitamente verdadeiro num if. E as consequências aparecem na primeira conta e na primeira comparação:

js
const total = localStorage.getItem('total');
const frete = localStorage.getItem('freteGratis');

console.log('soma:', total + 10);
console.log('condicao:', frete === true, '|', frete === 'true');
soma: 199.910 condicao: false | true

199.910 é "199.9" concatenado com 10. E frete === true é false, porque está comparando string com booleano. Converter na leitura resolve os dois: Number(localStorage.getItem('total')) e localStorage.getItem('freteGratis') === 'true'.

Objeto: JSON.stringify na ida, JSON.parse na volta

js
const carrinho = {
  cliente: 'Ana Souza',
  itens: [
    { id: 7712, nome: 'Teclado mecânico', preco: 289.9, qtd: 1 },
    { id: 8830, nome: 'Mouse sem fio', preco: 149.9, qtd: 2 },
  ],
};

localStorage.setItem('carrinho', JSON.stringify(carrinho));
console.log(localStorage.getItem('carrinho'));

const salvo = JSON.parse(localStorage.getItem('carrinho'));
console.log(salvo.itens.length, 'itens |', salvo.itens[0].nome);

const total = salvo.itens.reduce((soma, i) => soma + i.preco * i.qtd, 0);
console.log('total: R$', total.toFixed(2));
{"cliente":"Ana Souza","itens":[{"id":7712,"nome":"Teclado mecânico","preco":289.9,"qtd":1},{"id":8830,"nome":"Mouse sem fio","preco":149.9,"qtd":2}]} 2 itens | Teclado mecânico total: R$ 589.70

Esse é o par que resolve o rascunho de formulário: no submit que você viu em validar formulário com JavaScript, grave o objeto; no carregamento da página, leia e devolva os valores aos campos.

O que voltou do JSON.parse é um objeto novo, não o mesmo de antes. Isso importa: Date volta como string, undefined some, Map, Set e função não sobrevivem à viagem. Se o seu objeto tem uma data, guarde em ISO (new Date().toISOString()) e reconstrua na leitura.

getItem devolve null, nunca undefined

js
console.log('chave que nunca existiu:', localStorage.getItem('endereco'));
console.log('typeof:', typeof localStorage.getItem('endereco'));
console.log('length:', localStorage.length, '| key(0):', localStorage.key(0));

localStorage.removeItem('cupom');
console.log('depois do removeItem:', localStorage.length);

localStorage.clear();
console.log('depois do clear:', localStorage.length);
chave que nunca existiu: null typeof: object length: 4 | key(0): cliente depois do removeItem: 3 depois do clear: 0

A diferença entre null e undefined parece detalhe até você escrever if (valor === undefined) e o if nunca entrar. Use === null, ou o optional chaining e o ??, que tratam os dois de uma vez.

Há uma coincidência feliz aqui: JSON.parse(null) devolve null em vez de estourar, porque null vira a string "null", que é JSON válido.

js
console.log('JSON.parse(null) =>', JSON.parse(null));
console.log('JSON.parse(localStorage.getItem("inexistente")) =>', JSON.parse(localStorage.getItem('inexistente')));
JSON.parse(null) => null JSON.parse(localStorage.getItem("inexistente")) => null

Ou seja: ler uma chave que não existe não quebra. Ler uma chave com conteúdo corrompido, sim — e é o próximo assunto.

É síncrono, e isso é um problema de performance

Não há await, não há callback: a linha seguinte já enxerga o dado.

js
localStorage.setItem('carrinho', JSON.stringify(carrinho));
console.log('disponível na linha seguinte?', localStorage.getItem('carrinho') !== null);
disponível na linha seguinte? true

A simplicidade tem preço: enquanto o setItem acontece, a thread principal está parada — nada de renderizar, nada de responder a clique. Com 200 bytes ninguém percebe. Com um histórico de pedidos inteiro, percebe.

js
const historico = Array.from({ length: 5000 }, (_, i) => ({
  id: 7000 + i, nome: 'Teclado mecânico RGB', preco: 289.9, comprado: '2026-07-14',
}));

const json = JSON.stringify(historico);
console.log('pedidos          :', historico.length);
console.log('caracteres       :', json.length);
console.log('MB aprox (UTF-16):', (json.length * 2 / 1024 / 1024).toFixed(2));
pedidos : 5000 caracteres : 402001 MB aprox (UTF-16): 0.77

Cinco mil pedidos já ocupam 0,77 MB de um orçamento que gira em torno de 5 MB por origem na maioria dos navegadores — número que não está na especificação e varia entre eles. Passou do limite, o setItem lança QuotaExceededError.

guardar em tamanho típico vai no request some quando
localStorage ~5 MB por origem não o usuário limpa os dados do site
sessionStorage ~5 MB por aba não a aba fecha
cookie ~4 KB por cookie sim, em toda requisição expira ou é apagado
IndexedDB centenas de MB não o usuário limpa os dados do site

Se o dado é grande ou binário, o destino é IndexedDB — que é assíncrono justamente por isso.

Erros comuns

"[object Object]" is not valid JSON

O erro mais frequente da API inteira: guardar objeto sem JSON.stringify.

js
const carrinho = { cliente: 'Ana Souza', itens: [{ nome: 'Teclado mecânico', preco: 289.9 }] };

localStorage.setItem('carrinho', carrinho);
console.log('guardado:', localStorage.getItem('carrinho'));

const salvo = JSON.parse(localStorage.getItem('carrinho'));
console.log(salvo.itens.length);
guardado: [object Object] <anonymous_script>:1 [object Object] ^

SyntaxError: “[object Object]” is not valid JSON at JSON.parse (<anonymous>) at file:///private/tmp/loja/carrinho-storage.mjs:8:20 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)

Node.js v24.16.0

Repare que o setItem não reclamou. Ele converteu o objeto com o toString() padrão, que produz a string literal [object Object], e guardou isso com toda a tranquilidade. O erro só aparece no JSON.parse, que pode ser em outro arquivo, em outra sessão, uma semana depois. Se você viu [object Object] em qualquer lugar, procure por um stringify faltando.

Confiar que o conteúdo salvo continua válido

O usuário pode editar o localStorage pelo DevTools, uma versão antiga do seu código pode ter salvado outro formato, um setItem pode ter sido interrompido. JSON.parse num conteúdo quebrado derruba a página inteira — então a leitura sempre vai dentro de um try:

js
function lerJson(chave, padrao) {
  const bruto = localStorage.getItem(chave);
  if (bruto === null) return padrao;

  try {
    return JSON.parse(bruto);
  } catch {
    localStorage.removeItem(chave);
    return padrao;
  }
}

localStorage.setItem('carrinho', JSON.stringify({ itens: [{ nome: 'Teclado mecânico' }] }));
localStorage.setItem('cupom', '{quebrado');

console.log(lerJson('carrinho', { itens: [] }).itens.length);
console.log(lerJson('cupom', null));
console.log(lerJson('endereco', { uf: 'SP' }));
console.log('cupom quebrado foi apagado?', localStorage.getItem('cupom') === null);
1 null { uf: 'SP' } cupom quebrado foi apagado? true

Três caminhos cobertos numa função de dez linhas: chave válida, chave corrompida (que é apagada em vez de ficar quebrando para sempre) e chave ausente.

Assumir que localStorage existe

Em aba anônima de alguns navegadores, com cookies de terceiros bloqueados ou dentro de um <iframe> de outra origem, o simples acesso a localStorage lança exceção. Um teste de escrita resolve:

js
function storageDisponivel() {
  try {
    localStorage.setItem('__teste__', '1');
    localStorage.removeItem('__teste__');
    return true;
  } catch {
    return false;
  }
}

console.log('localStorage utilizável?', storageDisponivel());
localStorage utilizável? true

Se der false, o site precisa continuar funcionando — sem rascunho salvo, mas funcionando.

Como usar sem se arrepender

  • Uma chave com versão: carrinho:v1. Quando o formato mudar, você lê a antiga, converte e escreve a nova, sem quebrar quem já tinha dados.
  • Prefixe por domínio do problema: loja:carrinho, loja:endereco. O localStorage é compartilhado por todo o site, incluindo scripts de terceiros.
  • Uma função para ler, uma para gravar, ambas com try. Nunca JSON.parse espalhado pelo código.
  • Nada sensível. Token, CPF, endereço completo — qualquer script da página lê tudo.
  • Não é banco de dados. É cache e conveniência: rascunho de formulário, tema escolhido, carrinho antes do login. A fonte da verdade fica no servidor.

A próxima lição sai da memória e vai para a barra de endereço: window.location e redirecionamento, onde a query string vira dado. O guia completo de JavaScript mostra a trilha inteira em ordem.

  • localstorage
  • json
  • navegador
  • persistencia
  • webstorage

Perguntas frequentes

Quanto tempo os dados ficam no localStorage?
Até alguém apagar. Fechar a aba, fechar o navegador ou reiniciar a máquina não limpa nada. Some quando o usuário limpa os dados do site, quando seu código chama removeItem ou clear, ou em modo anônimo ao fechar a janela.
Posso guardar o token de login no localStorage?
Pode tecnicamente, e é uma decisão discutível. Qualquer script que rode na página lê o localStorage inteiro — inclusive um script de terceiro comprometido. Para sessão, cookie httpOnly definido pelo servidor é melhor.
Qual a diferença para o sessionStorage?
A API é idêntica; muda o tempo de vida e o alcance. sessionStorage morre quando a aba fecha e não é compartilhado entre abas, mesmo do mesmo site. localStorage sobrevive e é o mesmo para todas as abas daquela origem.
Dá para saber quando outra aba mudou o localStorage?
Dá: o evento storage no window dispara nas outras abas da mesma origem, com key, oldValue e newValue. Ele não dispara na aba que fez a mudança, o que é justamente o que você quer para sincronizar carrinho entre abas.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Window: propriedade localStorage — developer.mozilla.org
  2. MDN — Usando a API Web Storage — developer.mozilla.org
  3. HTML Standard — Web storage — html.spec.whatwg.org

Continue por aqui