Como salvar preferências do usuário com localStorage
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.
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.