Imagine que o usuário passou dez minutos preenchendo uma lista de tarefas na sua aplicação. Ele fecha a aba acidentalmente e, ao abrir de novo, tudo sumiu. Frustração garantida.
Ou imagine um e-commerce onde o carrinho de compras esvazia toda vez que o usuário navega para outra página. Inaceitável.
É para resolver problemas como esses que existem o LocalStorage e o SessionStorage — duas APIs nativas do navegador que permitem salvar dados diretamente no dispositivo do usuário, sem precisar de um servidor ou banco de dados.
A diferença fundamental
Ambos funcionam de forma idêntica em termos de API, mas diferem em persistência:
| LocalStorage | SessionStorage | |
|---|---|---|
| Duração | Permanente — fica até ser deletado manualmente | Temporário — some quando a aba/janela fecha |
| Escopo | Compartilhado entre todas as abas do mesmo domínio | Exclusivo da aba atual |
| Capacidade | ~5MB por domínio | ~5MB por aba |
| Uso típico | Preferências, login, carrinho | Dados de sessão, formulários temporários |
A API — simples e direta
Os quatro métodos que você vai usar o tempo todo:
// Salvar
localStorage.setItem("chave", "valor");
// Ler
const valor = localStorage.getItem("chave");
// Remover um item
localStorage.removeItem("chave");
// Limpar tudo
localStorage.clear();
// Ver quantos itens há
console.log(localStorage.length);
// Iterar sobre todas as chaves
for (let i = 0; i < localStorage.length; i++) {
const chave = localStorage.key(i);
const valor = localStorage.getItem(chave);
console.log(`${chave}: ${valor}`);
}
O sessionStorage tem exatamente a mesma API — basta trocar localStorage por sessionStorage.
O detalhe mais importante — tudo é string
O LocalStorage só armazena strings. Se você tentar salvar um número, boolean, array ou objeto, ele será convertido para string automaticamente — e de forma silenciosa, causando bugs difíceis de rastrear:
// ❌ Comportamento inesperado
localStorage.setItem("ativo", true);
const ativo = localStorage.getItem("ativo");
console.log(ativo); // "true" — string, não boolean!
console.log(ativo === true); // false — bug!
localStorage.setItem("quantidade", 42);
const qtd = localStorage.getItem("quantidade");
console.log(qtd + 1); // "421" — concatenação de string, não soma!
// Objeto vira string inútil
localStorage.setItem("usuario", { nome: "Ana" });
console.log(localStorage.getItem("usuario")); // "[object Object]"
A solução — JSON.stringify e JSON.parse
Sempre que salvar dados que não sejam strings simples, converta para JSON:
// ✅ Salvando objetos e arrays corretamente
// Salvar
const usuario = { nome: "Ana", idade: 28, premium: true };
localStorage.setItem("usuario", JSON.stringify(usuario));
// Ler — sempre parse ao recuperar
const dadosSalvos = localStorage.getItem("usuario");
const usuarioRecuperado = JSON.parse(dadosSalvos);
console.log(usuarioRecuperado.nome); // "Ana"
console.log(usuarioRecuperado.premium); // true — boolean de verdade!
// Arrays funcionam da mesma forma
const tarefas = ["Estudar", "Praticar", "Construir"];
localStorage.setItem("tarefas", JSON.stringify(tarefas));
const tarefasRecuperadas = JSON.parse(localStorage.getItem("tarefas"));
console.log(tarefasRecuperadas); // ["Estudar", "Praticar", "Construir"]
Tratamento de erros ao ler
O JSON.parse lança um erro se o valor salvo não for um JSON válido — o que pode acontecer se os dados foram corrompidos ou salvos incorretamente:
function lerDoStorage(chave, valorPadrao = null) {
try {
const item = localStorage.getItem(chave);
if (item === null) return valorPadrao;
return JSON.parse(item);
} catch (erro) {
console.error(`Erro ao ler "${chave}" do localStorage:`, erro);
return valorPadrao;
}
}
function salvarNoStorage(chave, valor) {
try {
localStorage.setItem(chave, JSON.stringify(valor));
return true;
} catch (erro) {
// Pode falhar se o storage estiver cheio
console.error(`Erro ao salvar "${chave}" no localStorage:`, erro);
return false;
}
}
// Uso limpo e seguro
const preferencias = lerDoStorage("preferencias", { tema: "claro", idioma: "pt-BR" });
salvarNoStorage("preferencias", { ...preferencias, tema: "escuro" });
Encapsular as operações de storage em funções utilitárias é uma ótima prática — você trata o erro uma vez e usa com segurança em todo o código.
Criando um helper de storage reutilizável
const storage = {
get(chave, padrao = null) {
try {
const item = localStorage.getItem(chave);
return item !== null ? JSON.parse(item) : padrao;
} catch {
return padrao;
}
},
set(chave, valor) {
try {
localStorage.setItem(chave, JSON.stringify(valor));
return true;
} catch {
return false;
}
},
remove(chave) {
localStorage.removeItem(chave);
},
clear() {
localStorage.clear();
},
existe(chave) {
return localStorage.getItem(chave) !== null;
},
};
// Uso
storage.set("usuario", { nome: "Pedro", plano: "pro" });
const usuario = storage.get("usuario");
console.log(usuario.nome); // "Pedro"
storage.set("visitas", (storage.get("visitas", 0)) + 1);
console.log(storage.get("visitas")); // 1, 2, 3...
Evento storage — sincronizando abas
Uma funcionalidade pouco conhecida: o evento storage dispara em outras abas do mesmo domínio quando o localStorage muda. Isso permite sincronizar estado entre abas:
// Em qualquer aba — detecta mudanças feitas por outras abas
window.addEventListener("storage", (evento) => {
console.log("Chave alterada:", evento.key);
console.log("Valor antigo:", evento.oldValue);
console.log("Novo valor:", evento.newValue);
console.log("URL de origem:", evento.url);
// Exemplo: sincronizar tema entre abas
if (evento.key === "tema") {
aplicarTema(JSON.parse(evento.newValue));
}
});
Exemplo completo — To-Do List com persistência
Vamos evoluir a To-Do List do artigo anterior adicionando persistência com LocalStorage:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>To-Do com Persistência</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #f0f2f5;
display: flex;
justify-content: center;
padding: 2rem 1rem;
}
.app {
background: white;
border-radius: 12px;
padding: 2rem;
width: 100%;
max-width: 500px;
box-shadow: 0 4px 24px rgba(0,0,0,.08);
}
h1 { font-size: 1.5rem; margin-bottom: 1.5rem; color: #1a1a2e; }
.topo {
display: flex;
gap: .5rem;
margin-bottom: 1rem;
}
input[type="text"] {
flex: 1;
padding: .65rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
}
input[type="text"]:focus { outline: none; border-color: #5c6bc0; }
.btn-add {
padding: .65rem 1.2rem;
background: #5c6bc0;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 700;
font-size: 1rem;
}
.filtros {
display: flex;
gap: .5rem;
margin-bottom: 1rem;
}
.filtro {
padding: .35rem .85rem;
border: 2px solid #e0e0e0;
background: white;
border-radius: 999px;
cursor: pointer;
font-size: .85rem;
font-weight: 600;
color: #666;
transition: all .2s;
}
.filtro.ativo {
background: #5c6bc0;
border-color: #5c6bc0;
color: white;
}
.info {
display: flex;
justify-content: space-between;
font-size: .85rem;
color: #888;
margin-bottom: 1rem;
}
.btn-acao {
background: none;
border: none;
color: #e53935;
cursor: pointer;
font-size: .85rem;
text-decoration: underline;
}
ul { list-style: none; }
.tarefa {
display: flex;
align-items: center;
gap: .75rem;
padding: .75rem 1rem;
border-radius: 8px;
margin-bottom: .5rem;
background: #f8f9ff;
border: 1px solid #e8eaf6;
animation: entrar .2s ease;
}
@keyframes entrar {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
.tarefa.concluida .texto {
text-decoration: line-through;
color: #aaa;
}
input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #5c6bc0;
}
.texto { flex: 1; }
.data {
font-size: .75rem;
color: #bbb;
}
.btn-del {
background: none;
border: none;
color: #ddd;
cursor: pointer;
font-size: 1.2rem;
transition: color .2s;
}
.btn-del:hover { color: #e53935; }
.vazio {
text-align: center;
color: #ccc;
padding: 2rem;
}
.badge-storage {
font-size: .75rem;
background: #e8eaf6;
color: #5c6bc0;
padding: .2rem .6rem;
border-radius: 999px;
margin-top: 1.5rem;
text-align: center;
}
</style>
</head>
<body>
<div class="app">
<h1>📝 Tarefas</h1>
<div class="topo">
<input type="text" id="input" placeholder="Nova tarefa...">
<button class="btn-add" id="btn-add">+</button>
</div>
<div class="filtros">
<button class="filtro ativo" data-filtro="todas">Todas</button>
<button class="filtro" data-filtro="pendentes">Pendentes</button>
<button class="filtro" data-filtro="concluidas">Concluídas</button>
</div>
<div class="info">
<span id="contador"></span>
<button class="btn-acao" id="btn-limpar">Limpar concluídas</button>
</div>
<ul id="lista"></ul>
<p class="badge-storage" id="badge">💾 Dados salvos no LocalStorage</p>
</div>
<script>
// ── Helper de storage ──────────────────────────────
const storage = {
get: (chave, padrao = null) => {
try {
const item = localStorage.getItem(chave);
return item !== null ? JSON.parse(item) : padrao;
} catch { return padrao; }
},
set: (chave, valor) => {
try {
localStorage.setItem(chave, JSON.stringify(valor));
return true;
} catch { return false; }
},
};
// ── Estado ─────────────────────────────────────────
let tarefas = storage.get("tarefas-app", []);
let filtroAtivo = storage.get("filtro-app", "todas");
let proximoId = storage.get("proximo-id", 1);
// ── Referências ────────────────────────────────────
const input = document.querySelector("#input");
const btnAdd = document.querySelector("#btn-add");
const lista = document.querySelector("#lista");
const contador = document.querySelector("#contador");
const btnLimpar = document.querySelector("#btn-limpar");
const badge = document.querySelector("#badge");
const filtros = document.querySelectorAll(".filtro");
// ── Persistência ───────────────────────────────────
function salvarEstado() {
storage.set("tarefas-app", tarefas);
storage.set("filtro-app", filtroAtivo);
storage.set("proximo-id", proximoId);
atualizarBadge();
}
function atualizarBadge() {
const bytes = JSON.stringify(tarefas).length;
const kb = (bytes / 1024).toFixed(2);
badge.textContent = `💾 ${tarefas.length} tarefa(s) salva(s) no LocalStorage · ${kb} KB`;
}
// ── Lógica ─────────────────────────────────────────
function tarefasFiltradas() {
switch (filtroAtivo) {
case "pendentes": return tarefas.filter(t => !t.concluida);
case "concluidas": return tarefas.filter(t => t.concluida);
default: return tarefas;
}
}
function adicionar() {
const texto = input.value.trim();
if (!texto) { input.focus(); return; }
tarefas.push({
id: proximoId++,
texto,
concluida: false,
criadaEm: new Date().toLocaleDateString("pt-BR"),
});
input.value = "";
input.focus();
salvarEstado();
renderizar();
}
function alternar(id) {
tarefas = tarefas.map(t =>
t.id === id ? { ...t, concluida: !t.concluida } : t
);
salvarEstado();
renderizar();
}
function remover(id) {
tarefas = tarefas.filter(t => t.id !== id);
salvarEstado();
renderizar();
}
function limparConcluidas() {
tarefas = tarefas.filter(t => !t.concluida);
salvarEstado();
renderizar();
}
// ── Renderização ───────────────────────────────────
function renderizar() {
const visiveis = tarefasFiltradas();
lista.innerHTML = "";
if (visiveis.length === 0) {
lista.innerHTML = `<li class="vazio">
${filtroAtivo === "todas"
? "Nenhuma tarefa ainda. Adicione uma acima!"
: `Nenhuma tarefa ${filtroAtivo} no momento.`}
</li>`;
} else {
const fragment = document.createDocumentFragment();
visiveis.forEach(tarefa => {
const li = document.createElement("li");
li.classList.add("tarefa");
if (tarefa.concluida) li.classList.add("concluida");
const check = document.createElement("input");
check.type = "checkbox";
check.checked = tarefa.concluida;
check.addEventListener("change", () => alternar(tarefa.id));
const span = document.createElement("span");
span.classList.add("texto");
span.textContent = tarefa.texto;
const data = document.createElement("span");
data.classList.add("data");
data.textContent = tarefa.criadaEm;
const btnDel = document.createElement("button");
btnDel.classList.add("btn-del");
btnDel.textContent = "×";
btnDel.title = "Remover";
btnDel.addEventListener("click", () => remover(tarefa.id));
li.append(check, span, data, btnDel);
fragment.appendChild(li);
});
lista.appendChild(fragment);
}
// Atualiza contador
const total = tarefas.length;
const concluidas = tarefas.filter(t => t.concluida).length;
contador.textContent = `${total - concluidas} pendente(s) · ${concluidas} concluída(s)`;
// Atualiza filtros ativos
filtros.forEach(btn => {
btn.classList.toggle("ativo", btn.dataset.filtro === filtroAtivo);
});
}
// ── Eventos ────────────────────────────────────────
btnAdd.addEventListener("click", adicionar);
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") adicionar();
});
btnLimpar.addEventListener("click", limparConcluidas);
filtros.forEach(btn => {
btn.addEventListener("click", () => {
filtroAtivo = btn.dataset.filtro;
salvarEstado();
renderizar();
});
});
// Sincroniza se outra aba alterar o storage
window.addEventListener("storage", (e) => {
if (e.key === "tarefas-app") {
tarefas = JSON.parse(e.newValue) || [];
renderizar();
}
});
// ── Inicialização ──────────────────────────────────
renderizar();
input.focus();
</script>
</body>
</html>
Abra esta página, adicione tarefas, feche e reabra — seus dados estarão lá. Abra em duas abas diferentes e observe a sincronização em tempo real.
Limitações e alternativas
O LocalStorage é poderoso para casos simples, mas tem limitações importantes:
Limitações:
- Apenas strings (resolvido com JSON)
- ~5MB por domínio (não use para imagens ou dados grandes)
- Síncrono — pode bloquear a thread em grandes volumes
- Não funciona em modo privado em alguns browsers
- Não é criptografado — nunca salve senhas ou tokens sensíveis
Para casos mais avançados, considere:
// IndexedDB — banco de dados completo no navegador
// Assíncrono, suporta objetos complexos, muito mais espaço
// Complexo de usar diretamente — use a biblioteca idb
// Cookies — persistência controlada, enviados ao servidor
// Úteis para autenticação
// Cache API — parte do Service Worker
// Ideal para PWAs e funcionamento offline
Para a maioria dos casos do dia a dia, LocalStorage é suficiente e prático.
Boas práticas
// ✅ 1. Use prefixo nas chaves para evitar conflitos
localStorage.setItem("minhaApp:usuario", JSON.stringify(usuario));
localStorage.setItem("minhaApp:configuracoes", JSON.stringify(config));
// ✅ 2. Sempre tenha valor padrão ao ler
const tema = storage.get("tema", "claro");
// ✅ 3. Nunca salve informações sensíveis
// ❌ Jamais faça isso:
localStorage.setItem("senha", "minhasenha123");
localStorage.setItem("token", "eyJhbGciOiJIUzI1NiJ9...");
// ✅ 4. Trate erros — o storage pode estar desabilitado
// (modo privado, políticas de segurança, storage cheio)
// ✅ 5. Versione seus dados para migrações futuras
const VERSAO = "2";
const versaoSalva = localStorage.getItem("minhaApp:versao");
if (versaoSalva !== VERSAO) {
localStorage.clear(); // dados incompatíveis — limpa e recomeça
localStorage.setItem("minhaApp:versao", VERSAO);
}
Tarefa para você
Construa um bloco de notas persistente com:
- Campo de texto grande (
<textarea>) onde o usuário escreve - Salvar automaticamente no LocalStorage a cada 2 segundos (se houve mudança)
- Exibir data e hora do último salvamento
- Botão "Nova nota" que limpa o campo (com confirmação)
- Contador de caracteres em tempo real
- Ao carregar a página, restaurar o último conteúdo salvo
// Dica: use setTimeout/clearTimeout para o salvamento automático
let timerSalvar;
textarea.addEventListener("input", () => {
clearTimeout(timerSalvar);
timerSalvar = setTimeout(() => {
salvar();
}, 2000);
});
Conclusão
Neste artigo você aprendeu:
- A diferença entre LocalStorage e SessionStorage
- A API completa —
setItem,getItem,removeItem,clear - Por que tudo deve ser serializado com
JSON.stringifyeJSON.parse - Como encapsular operações de storage em um helper seguro
- O evento
storagepara sincronização entre abas - Como construir uma To-Do List com persistência real
- Limitações do LocalStorage e quando considerar alternativas
- Boas práticas de segurança e organização
No próximo artigo vamos aprender sobre temporizadores com setTimeout e setInterval — recursos essenciais para animações, atualizações periódicas e operações com delay.
📌 Próximo artigo: Aula 16 — Temporizadores: setTimeout e setInterval
📚 Fontes e Referências
- MDN Web Docs — Window.localStorage: https://developer.mozilla.org/pt-BR/docs/Web/API/Window/localStorage
- MDN Web Docs — Window.sessionStorage: https://developer.mozilla.org/pt-BR/docs/Web/API/Window/sessionStorage
- MDN Web Docs — Storage API: https://developer.mozilla.org/pt-BR/docs/Web/API/Storage
- MDN Web Docs — StorageEvent: https://developer.mozilla.org/en-US/docs/Web/API/StorageEvent
- JavaScript.info — LocalStorage and sessionStorage: https://javascript.info/localstorage
- web.dev — Storage for the Web: https://web.dev/storage-for-the-web
- Eloquent JavaScript, Cap. 18 — HTTP and Forms: https://eloquentjavascript.net/18_http.html
- OWASP — HTML5 Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html