Stash da Ana
javascriptbrowserlocalStoragetutorial

Como salvar preferências do usuário com localStorage

3 min de leitura

Variáveis JavaScript vivem só enquanto a aba está aberta. Para lembrar uma escolha do usuário entre visitas — o tema escolhido, o idioma preferido, se ele já fechou aquele banner — é preciso um lugar persistente. O localStorage é o mais simples dos lugares persistentes que o browser oferece.

Existem alternativas: cookies (vão e voltam em toda requisição, custam banda e atrapalham cache de CDN), sessionStorage (some quando a aba fecha), IndexedDB (poderoso, mas com API assíncrona que pesa demais para uma única string). Para preferências, localStorage resolve.

O que é localStorage

É um dicionário de chave-valor que o browser mantém por origem (protocolo + domínio + porta). O conteúdo continua lá depois de fechar o browser, depois de reiniciar a máquina, e só some se o usuário limpar dados do site explicitamente. Cada origem tem em torno de 5 MB — limite enorme para preferências, irrisório para qualquer outra coisa.

A API tem três operações:

localStorage.setItem('tema', 'escuro')
const tema = localStorage.getItem('tema') // 'escuro'
localStorage.removeItem('tema')

Tudo é string. Objetos passam por JSON.stringify na escrita e JSON.parse na leitura — sem mágica.

Como o blog usa isso

A preferência de tema é persistida no momento do clique no botão de alternância:

const toggleTheme = () => {
  const next: Theme = theme === 'dark' ? 'light' : 'dark'
  setTheme(next)
  localStorage.setItem('theme', next)
  document.documentElement.classList.toggle('dark', next === 'dark')
}

Três efeitos no mesmo lugar: o estado do React muda, o valor é gravado, e a classe dark é adicionada (ou removida) do <html> para o Tailwind aplicar o tema. Manter os três juntos torna difícil que saiam de sincronia.

Chrome DevTools mostrando a chave 'theme' no Local Storage do blog Em DevTools > Application > Local Storage, dá para ver a chave theme sendo escrita em tempo real quando o botão é clicado.

Lendo a preferência na próxima visita

Salvar não basta — é preciso ler quando a página carrega de novo. Como localStorage é uma API do browser e o Next.js executa código no servidor durante o build, acessá-lo no corpo do componente quebra o build com localStorage is not defined. A leitura precisa acontecer dentro de um useEffect, que só roda no cliente, depois da hidratação:

useEffect(() => {
  const stored = localStorage.getItem('theme') as Theme | null
  const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches
    ? 'dark'
    : 'light'
  const resolved = stored ?? preferred
  setTheme(resolved)
  document.documentElement.classList.toggle('dark', resolved === 'dark')
}, [])

Se existe valor salvo, ele vence. Se não existe, cai para a preferência do sistema operacional via matchMedia (assunto do post sobre prefers-color-scheme). O ?? lida com o caso null sem cair na pegadinha de ||, que trataria string vazia como ausência.

O preço dessa abordagem é um pequeno FOUC — um flash de tema errado entre o HTML renderizado no build e o useEffect rodar. Para o blog, aceitável; sites maiores costumam injetar um script bloqueante no <head> que lê o localStorage antes da primeira renderização, eliminando o flash.

Cuidados

Qualquer script rodando na mesma origem consegue ler tudo que está no localStorage — então nada de senha, token de sessão ou dado pessoal sensível. Em modo privado de alguns browsers, ou com cookies de terceiros bloqueados em contextos específicos, setItem pode lançar QuotaExceededError; envolver em try/catch é razoável quando a aplicação depende disso para funcionar. E a API é síncrona: leitura e escrita bloqueiam a thread principal. Para uma string de quatro caracteres, irrelevante; para serializar um objeto grande a cada keystroke, problema.

Para o caso de preferências simples, é difícil escolher errado: localStorage é a ferramenta certa pelo motivo certo.