Javascript

LocalStorage e SessionStorage Já leu

12 min de leitura

LocalStorage e SessionStorage
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 n

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:

  1. Campo de texto grande (<textarea>) onde o usuário escreve
  2. Salvar automaticamente no LocalStorage a cada 2 segundos (se houve mudança)
  3. Exibir data e hora do último salvamento
  4. Botão "Nova nota" que limpa o campo (com confirmação)
  5. Contador de caracteres em tempo real
  6. 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.stringify e JSON.parse
  • Como encapsular operações de storage em um helper seguro
  • O evento storage para 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

Comentários

Mais em Javascript

Criando e Removendo Elementos Dinamicamente
Criando e Removendo Elementos Dinamicamente

Nos artigos anteriores aprendemos a selecionar elementos existentes no HTML e...

Tratamento de Erros com try, catch e finally
Tratamento de Erros com try, catch e finally

Todo programa que vai para produ&ccedil;&atilde;o vai encontrar situa&ccedil;...

O que é Programação Assíncrona? O Event Loop Explicado
O que é Programação Assíncrona? O Event Loop Explicado

Este &eacute; um dos artigos mais importantes de toda a s&eacute;rie. N&atild...