Imported from David-Li0406/meta-skill-evloving (
skill-flow/data/skills-refined-autoskill-36k/skillsmp/localstorage-patterns/SKILL.md). Install upstream withnpx skills add David-Li0406/meta-skill-evloving --skill localstorage-patterns. Copyright stays with the author.
localStorage Patterns
Padrões de Persistência Client-Side para Plataforma B2B de treinamento técnico corporativo
Versão: 1.0.0 Última Atualização: 2025-11-19 Target: localStorage API (Web Storage API) Projeto: Plataforma B2B de treinamento técnico corporativo
📋 Índice
- Overview
- Quando Usar Esta Skill
- Conceitos Fundamentais
- Padrões de Error Handling
- Quota Management
- Schema Design
- Exemplos Práticos do Projeto
- Antipadrões a Evitar
- Troubleshooting
- Referências
🎯 Overview
Esta skill documenta todos os padrões de uso do localStorage no Plataforma B2B de treinamento técnico corporativo, um sistema educacional que persiste notas de usuários e progresso de 227 módulos educacionais usando apenas client-side storage.
O que você vai aprender:
- Como tratar erros de localStorage de forma robusta (QuotaExceededError, SecurityError)
- Quando usar localStorage vs. sessionStorage vs. IndexedDB
- Padrões de schema design para JSON storage
- Estratégias de quota management (5-10MB limites)
- Sincronização localStorage ↔ React state
- Testes de localStorage em diferentes browsers
- Migração e versionamento de dados
Por que esta skill é importante:
- ✅ Resiliência: Previne perda de dados do usuário (notas, progresso)
- ✅ UX: Feedback claro quando storage está cheio ou bloqueado
- ✅ Manutenibilidade: Schema design facilita evolução
- ✅ Testabilidade: Padrões permitem mocks e testes
- ✅ Performance: Leitura/escrita otimizada (batching, debouncing)
Débito Técnico Atual (2025-11-19):
- ❌ Nenhum try/catch em operações localStorage (5 componentes NotesView)
- ❌ Sem tratamento de QuotaExceededError (usuário perde notas!)
- ❌ Sem limite de 50KB por nota (pode crashar)
- ❌ Sem fallback para sessionStorage ou memória
- ❌ Sem testes unitários de localStorage
Esta skill resolve: US-041, US-042, facilita US-043 (refatoração hooks)
📋 Quando Usar Esta Skill
Cenário 1: Implementando Auto-Save de Notas
Situação: Você precisa salvar notas do usuário automaticamente em <textarea> (CNotesView, BashNotesView, etc.)
Use esta skill para:
- Decidir quando salvar (onChange, onBlur, debounce?)
- Implementar try/catch robusto
- Tratar QuotaExceededError (storage cheio)
- Mostrar feedback ao usuário (salvando, salvo, erro)
- Sincronizar localStorage → React state
Exemplo de aplicação:
// ✅ Seguindo padrões da skill
function CNotesView({ courseId }) {
const [notes, setNotes, saveStatus] = useAutoSaveNotes(courseId);
return (
<div>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Suas anotações..."
/>
{saveStatus === 'saving' && <span>💾 Salvando...</span>}
{saveStatus === 'saved' && <span>✅ Salvo</span>}
{saveStatus === 'error' && <span>⚠️ Erro ao salvar</span>}
</div>
);
}
Cenário 2: Persistindo Progresso de Aulas
Situação: Você precisa salvar quais aulas o usuário completou (US-042)
Use esta skill para:
- Definir schema JSON (array de IDs vs. objeto com timestamps)
- Implementar operações CRUD (adicionar, remover, consultar)
- Tratar erros de leitura/escrita
- Migrar schema antigo → novo (versionamento)
- Sincronizar com UI (progresso visual)
Exemplo de aplicação:
// ✅ Usando hook customizado
function CLearningSystem() {
const [completedLessons, markComplete, progress] = useModuleProgress('c', 50);
const handleComplete = (lessonId) => {
try {
markComplete(lessonId);
} catch (error) {
if (error.name === 'QuotaExceededError') {
alert('Storage cheio! Limpe dados antigos.');
}
}
};
return (
<div>
<ProgressBar completed={progress.completed} total={progress.total} />
<LessonList completedIds={completedLessons} onComplete={handleComplete} />
</div>
);
}
Cenário 3: Debugging Storage Issues
Situação: Usuário reporta "notas não salvam" ou "progresso desaparece ao recarregar"
Use esta skill para:
- Diagnosticar se localStorage está disponível (private browsing?)
- Verificar quota disponível (Storage API)
- Inspecionar dados salvos (DevTools Application tab)
- Reproduzir bugs (simular quota excedida)
- Implementar logging de erros
Exemplo de debug:
// ✅ Diagnóstico completo
function diagnoseLocalStorage() {
// 1. Testar disponibilidade
if (typeof Storage === 'undefined') {
console.error('localStorage não suportado!');
return;
}
// 2. Testar leitura/escrita
try {
localStorage.setItem('test', 'value');
localStorage.removeItem('test');
console.log('✅ localStorage funcional');
} catch (e) {
console.error('❌ localStorage bloqueado:', e.name);
}
// 3. Verificar quota (Chrome 110+)
if (navigator.storage && navigator.storage.estimate) {
navigator.storage.estimate().then(estimate => {
const usedMB = (estimate.usage / 1024 / 1024).toFixed(2);
const quotaMB = (estimate.quota / 1024 / 1024).toFixed(2);
console.log(`Usando ${usedMB} MB de ${quotaMB} MB (${(estimate.usage / estimate.quota * 100).toFixed(1)}%)`);
});
}
// 4. Listar keys do Plataforma B2B de treinamento técnico corporativo
const keys = Object.keys(localStorage).filter(k => k.startsWith('plataforma-b2b_'));
console.log('Keys Plataforma B2B de treinamento técnico corporativo:', keys);
keys.forEach(key => {
const size = new Blob([localStorage.getItem(key)]).size;
console.log(`- ${key}: ${(size / 1024).toFixed(2)} KB`);
});
}
💡 Conceitos Fundamentais
1. localStorage vs. Outras Opções
Comparação:
| Feature | localStorage | sessionStorage | IndexedDB | Cookies |
|---|---|---|---|---|
| Tamanho | 5-10 MB | 5-10 MB | 50+ MB | 4 KB |
| Persistência | Permanente | Sessão | Permanente | Configurável |
| API | Síncrona | Síncrona | Assíncrona | Document.cookie |
| Performance | Rápida | Rápida | Rápida (indexed) | Lenta (HTTP) |
| Escopo | Origem | Tab/Window | Origem | Domínio |
| Uso | Preferências, notas | Estado temporário | DB client-side | Auth tokens |
Quando usar localStorage (nosso caso):
- ✅ Dados pequenos (<5MB total)
- ✅ Acesso síncrono (simplicidade)
- ✅ Persistência entre sessões
- ✅ Leitura/escrita frequente
- ✅ Sem necessidade de índices/queries complexas
Quando NÃO usar localStorage:
- ❌ Dados sensíveis (não é criptografado!)
- ❌ Dados >5MB (use IndexedDB)
- ❌ Queries complexas (use IndexedDB)
- ❌ Performance crítica para grandes datasets
2. Limites e Quotas
Limites por Browser (2025):
Chrome/Edge: 10 MB por origem
Firefox: 10 MB por origem
Safari: 5 MB por origem (mais restritivo!)
Opera: 10 MB por origem
Cálculo aproximado:
- 1 caractere = ~2 bytes (UTF-16)
- 10 MB = ~5.000.000 caracteres
- Nota de 50KB = ~25.000 caracteres (10 páginas A4)
Limites do Projeto Plataforma B2B de treinamento técnico corporativo:
- 📝 Máximo 50KB por nota (25.000 caracteres)
- 📊 5 sistemas × 50KB = 250KB (notas)
- 📈 227 módulos × 100 bytes = 22KB (progresso)
- 🎯 Total estimado: ~300KB (3% da quota)
Buffer de segurança: Alertar usuário ao atingir 80% da quota (8MB)
3. Erros Comuns
QuotaExceededError
Quando ocorre:
- Storage atingiu limite de 5-10MB
- Tentando salvar dado grande (>50KB)
- Safari em modo privado (quota 0!)
Como tratar:
try {
localStorage.setItem('plataforma-b2b_notes_c', longText);
} catch (error) {
if (error.name === 'QuotaExceededError') {
// Estratégias:
// 1. Limpar dados antigos
clearOldData();
// 2. Tentar salvar em sessionStorage (fallback)
sessionStorage.setItem('plataforma-b2b_notes_c_temp', longText);
// 3. Avisar usuário
showToast('⚠️ Storage cheio! Dados salvos temporariamente.', 'warning');
// 4. Oferecer download de backup
downloadBackup(longText, 'c-notes-backup.txt');
}
}
SecurityError
Quando ocorre:
- Navegação privada (Safari, Firefox)
- Configuração de segurança bloqueou storage
- Iframe cross-origin sem permissão
Como tratar:
function isLocalStorageAvailable() {
try {
const test = '__localStorage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (error) {
if (error.name === 'SecurityError') {
console.warn('localStorage bloqueado (private browsing?)');
return false;
}
throw error;
}
}
// Uso
if (!isLocalStorageAvailable()) {
// Fallback: usar memória (não persiste)
const inMemoryStorage = {};
// ... implementar API similar
}
DOMException
Quando ocorre:
- Tentar acessar localStorage em contexto inválido
- Worker thread (não tem acesso a localStorage)
- Browser muito antigo
Como tratar:
function safeGetItem(key, defaultValue = null) {
try {
return localStorage.getItem(key) ?? defaultValue;
} catch (error) {
if (error instanceof DOMException) {
console.error('DOMException ao acessar localStorage:', error);
return defaultValue;
}
throw error;
}
}
🛡️ Padrões de Error Handling
Padrão 1: Try/Catch em Todas Operações
Princípio: Nunca assumir que localStorage está disponível.
Implementação:
// ✅ SEMPRE fazer
function saveNotes(courseId, notes) {
const key = `plataforma-b2b_notes_${courseId}`;
try {
localStorage.setItem(key, notes);
return { success: true, error: null };
} catch (error) {
console.error(`Erro ao salvar notas (${key}):`, error);
// Classificar erro
if (error.name === 'QuotaExceededError') {
return { success: false, error: 'quota_exceeded' };
} else if (error.name === 'SecurityError') {
return { success: false, error: 'security_blocked' };
} else {
return { success: false, error: 'unknown' };
}
}
}
// ❌ NUNCA fazer
function saveNotesUnsafe(courseId, notes) {
localStorage.setItem(`plataforma-b2b_notes_${courseId}`, notes); // Pode crashar!
}
Padrão 2: Graceful Degradation
Princípio: Sistema continua funcionando mesmo se localStorage falhar.
Implementação:
// Hook com fallback para memória
function useAutoSaveNotes(courseId) {
const [notes, setNotes] = useState('');
const [saveStatus, setSaveStatus] = useState('idle');
const [storageAvailable, setStorageAvailable] = useState(true);
// Carregar ao montar
useEffect(() => {
try {
const saved = localStorage.getItem(`plataforma-b2b_notes_${courseId}`);
if (saved) setNotes(saved);
} catch (error) {
console.warn('localStorage indisponível, usando memória');
setStorageAvailable(false);
}
}, [courseId]);
// Auto-save debounced
useEffect(() => {
if (!storageAvailable) return; // Skip se bloqueado
const timer = setTimeout(() => {
setSaveStatus('saving');
try {
localStorage.setItem(`plataforma-b2b_notes_${courseId}`, notes);
setSaveStatus('saved');
} catch (error) {
console.error('Erro ao salvar:', error);
setSaveStatus('error');
if (error.name === 'QuotaExceededError') {
// Fallback: sessionStorage
sessionStorage.setItem(`plataforma-b2b_notes_${courseId}_temp`, notes);
}
}
}, 1000); // Debounce 1s
return () => clearTimeout(timer);
}, [notes, courseId, storageAvailable]);
return [notes, setNotes, saveStatus];
}
Padrão 3: User-Friendly Error Messages
Princípio: Usuário entende o problema e sabe como resolver.
Implementação:
function showStorageErrorToast(errorType) {
const messages = {
quota_exceeded: {
title: '⚠️ Armazenamento Cheio',
message: 'Suas notas estão muito grandes. Limite: 50KB por curso.',
action: 'Limpar Dados Antigos'
},
security_blocked: {
title: '🔒 Modo Privado Detectado',
message: 'Dados salvos apenas durante esta sessão.',
action: 'Entendi'
},
unknown: {
title: '❌ Erro ao Salvar',
message: 'Tente recarregar a página.',
action: 'Recarregar'
}
};
const config = messages[errorType] || messages.unknown;
// Usando biblioteca de toast (ex: react-hot-toast)
toast.error(
<div>
<strong>{config.title}</strong>
<p>{config.message}</p>
<button onClick={() => handleAction(errorType)}>
{config.action}
</button>
</div>,
{ duration: 8000 }
);
}
📊 Quota Management
Padrão 1: Verificar Quota Disponível
API Moderna (Chrome 110+, Firefox 115+):
async function getStorageInfo() {
if (!navigator.storage || !navigator.storage.estimate) {
return { supported: false };
}
const estimate = await navigator.storage.estimate();
return {
supported: true,
usedBytes: estimate.usage,
quotaBytes: estimate.quota,
usedMB: (estimate.usage / 1024 / 1024).toFixed(2),
quotaMB: (estimate.quota / 1024 / 1024).toFixed(2),
percentUsed: ((estimate.usage / estimate.quota) * 100).toFixed(1)
};
}
// Uso
const info = await getStorageInfo();
console.log(`Usando ${info.usedMB} MB de ${info.quotaMB} MB (${info.percentUsed}%)`);
if (parseFloat(info.percentUsed) > 80) {
alert('⚠️ Storage quase cheio! Considere limpar dados antigos.');
}
Padrão 2: Limitar Tamanho de Notas
Implementação:
const MAX_NOTE_SIZE_BYTES = 50 * 1024; // 50KB
function validateNoteSize(text) {
const sizeBytes = new Blob([text]).size;
const sizeKB = (sizeBytes / 1024).toFixed(2);
if (sizeBytes > MAX_NOTE_SIZE_BYTES) {
return {
valid: false,
size: sizeKB,
limit: '50.00',
message: `Nota muito grande (${sizeKB} KB). Limite: 50 KB.`
};
}
return { valid: true, size: sizeKB };
}
// Uso no componente
function CNotesView({ courseId }) {
const [notes, setNotes] = useState('');
const [sizeWarning, setSizeWarning] = useState(null);
const handleChange = (e) => {
const newText = e.target.value;
const validation = validateNoteSize(newText);
if (!validation.valid) {
setSizeWarning(validation.message);
return; // Bloquear input adicional
}
setSizeWarning(null);
setNotes(newText);
};
return (
<div>
<textarea value={notes} onChange={handleChange} maxLength={25000} />
{sizeWarning && <p className="text-red-600">{sizeWarning}</p>}
<p className="text-sm text-gray-500">
Tamanho: {(new Blob([notes]).size / 1024).toFixed(2)} KB / 50 KB
</p>
</div>
);
}
Padrão 3: Limpar Dados Antigos
Estratégia: Remover dados não acessados por 90+ dias.
Implementação:
function clearOldData(daysThreshold = 90) {
const now = Date.now();
const keys = Object.keys(localStorage);
let freedBytes = 0;
keys.forEach(key => {
if (!key.startsWith('plataforma-b2b_')) return;
try {
const data = JSON.parse(localStorage.getItem(key));
// Schema com timestamp
if (data && data.lastUpdated) {
const ageInDays = (now - data.lastUpdated) / (1000 * 60 * 60 * 24);
if (ageInDays > daysThreshold) {
const size = new Blob([localStorage.getItem(key)]).size;
localStorage.removeItem(key);
freedBytes += size;
console.log(`Removido: ${key} (${ageInDays.toFixed(0)} dias)`);
}
}
} catch (error) {
console.warn(`Erro ao processar ${key}:`, error);
}
});
const freedKB = (freedBytes / 1024).toFixed(2);
console.log(`Liberados ${freedKB} KB de armazenamento`);
return freedKB;
}
🗂️ Schema Design
Padrão 1: Versionamento de Schema
Princípio: Schemas evoluem, dados antigos devem migrar.
Implementação:
// Schema v1 (antigo)
{
"notes": "Texto das notas...",
"lastSaved": 1700000000000
}
// Schema v2 (novo - com versão)
{
"version": 2,
"notes": "Texto das notas...",
"metadata": {
"lastSaved": 1700000000000,
"lastUpdated": 1700000000000,
"wordCount": 150,
"characterCount": 850
}
}
// Função de migração
function migrateNotesSchema(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
try {
const data = JSON.parse(raw);
// Detectar versão
if (!data.version) {
// Migrar v1 → v2
const migrated = {
version: 2,
notes: data.notes || '',
metadata: {
lastSaved: data.lastSaved || Date.now(),
lastUpdated: Date.now(),
wordCount: (data.notes || '').split(/\s+/).length,
characterCount: (data.notes || '').length
}
};
localStorage.setItem(key, JSON.stringify(migrated));
console.log(`Migrado ${key} para v2`);
return migrated;
}
return data; // Já está na versão atual
} catch (error) {
console.error(`Erro ao migrar ${key}:`, error);
return null;
}
}
Padrão 2: Namespace de Keys
Princípio: Prefixar todas as keys para evitar conflitos.
Convenção do Projeto:
// Namespace: plataforma-b2b_<tipo>_<identificador>
// Notas por curso
'plataforma-b2b_notes_bash'
'plataforma-b2b_notes_c'
'plataforma-b2b_notes_rust'
// Progresso por curso
'plataforma-b2b_progress_bash'
'plataforma-b2b_progress_c'
'plataforma-b2b_progress_rust'
// Configurações globais
'plataforma-b2b_settings_theme'
'plataforma-b2b_settings_language'
// Sessão temporária
'plataforma-b2b_session_current_view'
// Função helper
function getKey(type, identifier) {
return `plataforma-b2b_${type}_${identifier}`;
}
// Uso
const key = getKey('notes', 'bash'); // 'plataforma-b2b_notes_bash'
Padrão 3: JSON Schema para Progresso
Estrutura recomendada:
// Schema para progresso de módulos
{
"version": 1,
"courseId": "c",
"totalModules": 50,
"completedModules": [
{ "id": "1.1", "completedAt": 1700000000000 },
{ "id": "1.2", "completedAt": 1700000100000 },
{ "id": "2.1", "completedAt": 1700000200000 }
],
"progress": {
"percentage": 6, // 3/50 * 100
"lastUpdated": 1700000200000
}
}
// Operações CRUD
class CourseProgress {
constructor(courseId, totalModules) {
this.key = `plataforma-b2b_progress_${courseId}`;
this.courseId = courseId;
this.totalModules = totalModules;
}
load() {
try {
const raw = localStorage.getItem(this.key);
return raw ? JSON.parse(raw) : this.getDefaultSchema();
} catch (error) {
console.error('Erro ao carregar progresso:', error);
return this.getDefaultSchema();
}
}
getDefaultSchema() {
return {
version: 1,
courseId: this.courseId,
totalModules: this.totalModules,
completedModules: [],
progress: { percentage: 0, lastUpdated: Date.now() }
};
}
markComplete(moduleId) {
const data = this.load();
// Evitar duplicatas
if (data.completedModules.some(m => m.id === moduleId)) {
return data;
}
data.completedModules.push({
id: moduleId,
completedAt: Date.now()
});
data.progress.percentage = Math.round(
(data.completedModules.length / this.totalModules) * 100
);
data.progress.lastUpdated = Date.now();
try {
localStorage.setItem(this.key, JSON.stringify(data));
} catch (error) {
if (error.name === 'QuotaExceededError') {
alert('Storage cheio! Não foi possível salvar progresso.');
}
throw error;
}
return data;
}
isCompleted(moduleId) {
const data = this.load();
return data.completedModules.some(m => m.id === moduleId);
}
reset() {
localStorage.removeItem(this.key);
}
}
// Uso
const cProgress = new CourseProgress('c', 50);
cProgress.markComplete('1.1');
console.log(cProgress.isCompleted('1.1')); // true
🔧 Exemplos Práticos do Projeto
Exemplo 1: CNotesView (Atual - SEM Error Handling)
Localização: src/components/CNotesView.jsx
Código Atual (ANTES - ❌ Problemático):
function CNotesView({ onBack }) {
const [notes, setNotes] = useState('');
useEffect(() => {
// ❌ Sem try/catch!
const savedNotes = localStorage.getItem('c-learning-notes');
if (savedNotes) {
setNotes(savedNotes);
}
}, []);
useEffect(() => {
// ❌ Sem try/catch!
// ❌ Sem debounce (salva a cada keystroke!)
localStorage.setItem('c-learning-notes', notes);
}, [notes]);
return (
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Minhas anotações sobre C..."
/>
);
}
Problemas:
- ❌ Sem try/catch (pode crashar em modo privado)
- ❌ Sem debounce (performance ruim com 1000+ keystrokes)
- ❌ Sem tratamento de QuotaExceededError
- ❌ Sem feedback ao usuário (salvando? erro?)
- ❌ Sem limite de tamanho (pode exceder quota)
Código Refatorado (DEPOIS - ✅ Robusto):
function CNotesView({ onBack }) {
const [notes, setNotes, saveStatus] = useAutoSaveNotes('c');
const [sizeInfo, setSizeInfo] = useState({ size: 0, percentage: 0 });
useEffect(() => {
const sizeBytes = new Blob([notes]).size;
const sizeKB = sizeBytes / 1024;
const percentage = (sizeBytes / (50 * 1024)) * 100;
setSizeInfo({ size: sizeKB.toFixed(2), percentage: percentage.toFixed(1) });
}, [notes]);
return (
<div className="notes-container">
<div className="notes-header">
<button onClick={onBack}>← Voltar</button>
<div className="save-status">
{saveStatus === 'saving' && <span>💾 Salvando...</span>}
{saveStatus === 'saved' && <span>✅ Salvo automaticamente</span>}
{saveStatus === 'error' && (
<span className="text-red-600">⚠️ Erro ao salvar</span>
)}
</div>
</div>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Minhas anotações sobre C..."
className="w-full h-96 p-4 border rounded"
/>
<div className="notes-footer text-sm text-gray-500">
<span>Tamanho: {sizeInfo.size} KB / 50 KB ({sizeInfo.percentage}%)</span>
{parseFloat(sizeInfo.percentage) > 80 && (
<span className="text-yellow-600 ml-4">
⚠️ Nota grande, considere dividir em arquivos menores
</span>
)}
</div>
</div>
);
}
Arquivo real: src/components/CNotesView.jsx:1-80
Exemplo 2: Hook useAutoSaveNotes (Novo - Para US-041)
Localização: src/hooks/useAutoSaveNotes.js (criar)
Implementação Completa:
import { useState, useEffect, useRef } from 'react';
/**
* Hook para auto-save de notas com error handling robusto
*
* @param {string} courseId - ID do curso ('c', 'bash', 'rust', etc.)
* @param {number} debounceMs - Delay para auto-save (padrão: 1000ms)
* @returns {[string, function, string]} [notes, setNotes, saveStatus]
*/
export function useAutoSaveNotes(courseId, debounceMs = 1000) {
const [notes, setNotes] = useState('');
const [saveStatus, setSaveStatus] = useState('idle'); // idle, saving, saved, error
const [storageAvailable, setStorageAvailable] = useState(true);
const timerRef = useRef(null);
const key = `plataforma-b2b_notes_${courseId}`;
// Carregar notas ao montar
useEffect(() => {
try {
const saved = localStorage.getItem(key);
if (saved) {
setNotes(saved);
setSaveStatus('saved');
}
} catch (error) {
console.error('Erro ao carregar notas:', error);
if (error.name === 'SecurityError') {
setStorageAvailable(false);
setSaveStatus('error');
}
}
}, [key]);
// Auto-save com debounce
useEffect(() => {
if (!storageAvailable) return;
// Limpar timer anterior
if (timerRef.current) {
clearTimeout(timerRef.current);
}
setSaveStatus('saving');
timerRef.current = setTimeout(() => {
try {
// Validar tamanho
const sizeBytes = new Blob([notes]).size;
const MAX_SIZE = 50 * 1024; // 50KB
if (sizeBytes > MAX_SIZE) {
console.warn(`Nota muito grande: ${(sizeBytes / 1024).toFixed(2)} KB`);
setSaveStatus('error');
// Tentar comprimir (remover espaços extras)
const compressed = notes.replace(/\s+/g, ' ').trim();
const compressedSize = new Blob([compressed]).size;
if (compressedSize <= MAX_SIZE) {
localStorage.setItem(key, compressed);
setSaveStatus('saved');
console.log('Nota comprimida e salva');
} else {
alert('⚠️ Nota muito grande! Limite: 50KB');
}
return;
}
localStorage.setItem(key, notes);
setSaveStatus('saved');
} catch (error) {
console.error('Erro ao salvar notas:', error);
setSaveStatus('error');
if (error.name === 'QuotaExceededError') {
// Fallback: sessionStorage
try {
sessionStorage.setItem(`${key}_temp`, notes);
alert('⚠️ Storage cheio! Nota salva temporariamente.');
} catch (e) {
alert('❌ Não foi possível salvar a nota.');
}
} else if (error.name === 'SecurityError') {
setStorageAvailable(false);
alert('🔒 Modo privado detectado. Notas não serão persistidas.');
}
}
}, debounceMs);
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [notes, key, debounceMs, storageAvailable]);
return [notes, setNotes, saveStatus];
}
Arquivo real: src/hooks/useAutoSaveNotes.js:1-95 (criar para US-041)
Exemplo 3: Hook useModuleProgress (Novo - Para US-042)
Localização: src/hooks/useModuleProgress.js (criar)
Implementação Completa:
import { useState, useEffect, useCallback } from 'react';
/**
* Hook para gerenciar progresso de módulos
*
* @param {string} courseId - ID do curso
* @param {number} totalModules - Total de módulos no curso
* @returns {[string[], function, object]} [completedIds, markComplete, progressInfo]
*/
export function useModuleProgress(courseId, totalModules) {
const [completed, setCompleted] = useState([]);
const key = `plataforma-b2b_progress_${courseId}`;
// Carregar progresso ao montar
useEffect(() => {
try {
const raw = localStorage.getItem(key);
if (raw) {
const data = JSON.parse(raw);
setCompleted(data.completedModules.map(m => m.id));
}
} catch (error) {
console.error('Erro ao carregar progresso:', error);
}
}, [key]);
// Marcar módulo como completo
const markComplete = useCallback((moduleId) => {
if (completed.includes(moduleId)) {
console.log(`Módulo ${moduleId} já completo`);
return;
}
const newCompleted = [...completed, moduleId];
setCompleted(newCompleted);
const data = {
version: 1,
courseId,
totalModules,
completedModules: newCompleted.map(id => ({
id,
completedAt: Date.now()
})),
progress: {
percentage: Math.round((newCompleted.length / totalModules) * 100),
lastUpdated: Date.now()
}
};
try {
localStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error('Erro ao salvar progresso:', error);
if (error.name === 'QuotaExceededError') {
alert('⚠️ Storage cheio! Progresso não salvo.');
}
}
}, [completed, courseId, totalModules, key]);
// Resetar progresso
const reset = useCallback(() => {
setCompleted([]);
try {
localStorage.removeItem(key);
} catch (error) {
console.error('Erro ao resetar progresso:', error);
}
}, [key]);
// Info de progresso
const progressInfo = {
completed: completed.length,
total: totalModules,
percentage: Math.round((completed.length / totalModules) * 100),
remaining: totalModules - completed.length
};
return [completed, markComplete, progressInfo, reset];
}
Arquivo real: src/hooks/useModuleProgress.js:1-80 (criar para US-042)
❌ Antipadrões a Evitar
1. Operações Síncronas Sem Try/Catch
❌ Não fazer:
// Pode crashar a aplicação!
const notes = localStorage.getItem('notes');
localStorage.setItem('notes', newNotes);
✅ Fazer:
try {
const notes = localStorage.getItem('notes');
localStorage.setItem('notes', newNotes);
} catch (error) {
console.error('Erro localStorage:', error);
// Fallback
}
2. Salvar a Cada Keystroke (Sem Debounce)
❌ Não fazer:
// 1000 keystrokes = 1000 escritas no localStorage!
<textarea onChange={(e) => {
localStorage.setItem('notes', e.target.value);
}} />
✅ Fazer:
// Debounce de 1 segundo
useEffect(() => {
const timer = setTimeout(() => {
localStorage.setItem('notes', notes);
}, 1000);
return () => clearTimeout(timer);
}, [notes]);
3. Salvar Dados Sensíveis
❌ Não fazer:
// localStorage NÃO é criptografado!
localStorage.setItem('user_password', password);
localStorage.setItem('credit_card', cardNumber);
✅ Fazer:
// Dados sensíveis: usar backend + HTTPS
await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
4. Usar localStorage em Loops
❌ Não fazer:
// 1000 iterações = 1000 escritas!
for (let i = 0; i < 1000; i++) {
localStorage.setItem(`item_${i}`, data[i]);
}
✅ Fazer:
// Batch: salvar tudo de uma vez
const batch = data.map((item, i) => ({ id: i, data: item }));
localStorage.setItem('items_batch', JSON.stringify(batch));
5. Não Versionar Schema
❌ Não fazer:
// Mudou a estrutura? Dados antigos quebram!
localStorage.setItem('progress', JSON.stringify({ ids: [1, 2, 3] }));
// 6 meses depois...
localStorage.setItem('progress', JSON.stringify({ modules: [{id: 1}] }));
✅ Fazer:
// Sempre incluir versão
const data = {
version: 2,
modules: [{ id: 1, completedAt: Date.now() }]
};
// Migrar ao carregar
function load() {
const raw = localStorage.getItem('progress');
const data = JSON.parse(raw);
if (data.version === 1) {
return migrateV1toV2(data);
}
return data;
}
🚨 Troubleshooting
Problema 1: "Notas não salvam em modo privado"
Sintoma:
- Usuário em Safari/Firefox modo privado
- Console:
SecurityError: The operation is insecure
Diagnóstico:
function isPrivateMode() {
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
return false;
} catch (e) {
return e.name === 'SecurityError';
}
}
if (isPrivateMode()) {
console.log('Modo privado detectado!');
}
Solução:
// Fallback para sessionStorage
function safeSetItem(key, value) {
try {
localStorage.setItem(key, value);
} catch (error) {
if (error.name === 'SecurityError') {
// Tentar sessionStorage
sessionStorage.setItem(key, value);
console.warn('Usando sessionStorage (modo privado)');
}
}
}
Problema 2: "Storage cheio (QuotaExceededError)"
Sintoma:
- Console:
QuotaExceededError: Failed to execute 'setItem' - Usuário com notas muito grandes
Diagnóstico:
// Calcular tamanho usado
function calculateTotalSize() {
let total = 0;
Object.keys(localStorage).forEach(key => {
total += new Blob([localStorage.getItem(key)]).size;
});
return (total / 1024 / 1024).toFixed(2); // MB
}
console.log(`Total usado: ${calculateTotalSize()} MB`);
Solução:
// Limpar dados antigos
function clearOldData() {
const keys = Object.keys(localStorage);
keys.forEach(key => {
if (!key.startsWith('plataforma-b2b_')) return;
try {
const data = JSON.parse(localStorage.getItem(key));
if (data.lastUpdated && Date.now() - data.lastUpdated > 90 * 24 * 60 * 60 * 1000) {
localStorage.removeItem(key);
console.log(`Removido: ${key}`);
}
} catch (e) {
// Ignorar dados inválidos
}
});
}
// Chamar antes de salvar
try {
localStorage.setItem('key', 'value');
} catch (error) {
if (error.name === 'QuotaExceededError') {
clearOldData();
// Tentar novamente
localStorage.setItem('key', 'value');
}
}
Problema 3: "Dados corruptos (JSON.parse falha)"
Sintoma:
- Console:
SyntaxError: Unexpected token in JSON - Dados não carregam
Diagnóstico:
function validateLocalStorageData() {
const keys = Object.keys(localStorage).filter(k => k.startsWith('plataforma-b2b_'));
keys.forEach(key => {
try {
JSON.parse(localStorage.getItem(key));
console.log(`✅ ${key} válido`);
} catch (error) {
console.error(`❌ ${key} corrompido:`, error);
localStorage.removeItem(key);
}
});
}
Solução:
function safeGetItem(key, defaultValue = null) {
try {
const raw = localStorage.getItem(key);
if (!raw) return defaultValue;
return JSON.parse(raw);
} catch (error) {
console.error(`Erro ao parsear ${key}:`, error);
// Remover dado corrompido
localStorage.removeItem(key);
return defaultValue;
}
}
// Uso
const notes = safeGetItem('plataforma-b2b_notes_c', '');
Problema 4: "Progresso desaparece ao recarregar"
Sintoma:
- Progresso salvo mas não aparece após F5
- localStorage vazio
Diagnóstico:
// Verificar se key está correta
console.log('Keys Plataforma B2B de treinamento técnico corporativo:', Object.keys(localStorage).filter(k => k.startsWith('plataforma-b2b_')));
// Verificar em outra tab (mesmo domínio)
// localStorage é compartilhado entre tabs da mesma origem
Causas comuns:
- Key incorreta (
plataforma-b2b_notes_cvsc-learning-notes) - Limpar storage ao desmontar (erro de código)
- Browser limpa localStorage (configuração)
Solução:
// Usar constantes para keys
const STORAGE_KEYS = {
notes: (courseId) => `plataforma-b2b_notes_${courseId}`,
progress: (courseId) => `plataforma-b2b_progress_${courseId}`
};
// Nunca limpar ao desmontar
useEffect(() => {
// ✅ Carregar
const notes = localStorage.getItem(STORAGE_KEYS.notes('c'));
// ❌ NÃO fazer cleanup de localStorage!
return () => {
// localStorage.removeItem(...); // ❌ Errado!
};
}, []);
Problema 5: "Performance ruim com muitas escritas"
Sintoma:
- UI trava ao digitar
- Muitas operações localStorage/segundo
Diagnóstico:
// Contar operações
let writeCount = 0;
const originalSetItem = localStorage.setItem;
localStorage.setItem = function(...args) {
writeCount++;
return originalSetItem.apply(this, args);
};
setInterval(() => {
console.log(`Escritas/segundo: ${writeCount}`);
writeCount = 0;
}, 1000);
Solução:
// Debounce obrigatório
function useDebouncedLocalStorage(key, value, delay = 1000) {
useEffect(() => {
const timer = setTimeout(() => {
localStorage.setItem(key, value);
}, delay);
return () => clearTimeout(timer);
}, [key, value, delay]);
}
📚 Referências
Skills Relacionadas
- react-components-patterns - Hooks customizados (useAutoSaveNotes)
- platform-architecture - Arquitetura e localStorage keys
- system-state-management - Sincronização localStorage ↔ React
Documentação Técnica
- docs/tecnico/storage/ - Documentação completa de storage (criar)
- ROADMAP.md - US-041, US-042 (user stories)
Código Real do Projeto
Componentes com localStorage (ANTES da refatoração):
src/components/CNotesView.jsx:45-60- Notas de C (sem error handling)src/components/BashNotesView.jsx:45-60- Notas de Bash (duplicado)src/components/RustNotesView.jsx:45-60- Notas de Rust (duplicado)src/components/VSCodeNotesView.jsx:45-60- Notas de VSCode (duplicado)src/components/ClaudeCodeNotesView.jsx:45-60- Notas de Claude Code (duplicado)
Hooks Customizados (DEPOIS da refatoração - US-041, US-042):
src/hooks/useAutoSaveNotes.js- Auto-save com error handlingsrc/hooks/useModuleProgress.js- Gerenciamento de progresso
Recursos Externos
- MDN - Web Storage API
- MDN - Storage API - Quota management
- Can I Use - localStorage - Suporte por browser
- Web.dev - Storage Limits - Limites detalhados
📝 Arquivos Auxiliares
Esta skill possui 3 arquivos auxiliares detalhados:
- error-handling.md - Guia completo de tratamento de erros
- quota-management.md - Estratégias de gerenciamento de quota
- troubleshooting.md - Diagnósticos e soluções para problemas comuns
📍 Você está em: .claude/skills/localStorage-patterns/SKILL.md
📅 Criado em: 2025-11-19
👤 Mantido por: João Pelegrino + Claude Code
🎯 Uso: Referência para implementar localStorage no projeto (US-041, US-042)
🔄 Última auditoria: 2025-11-19
📊 Auto-discovery score: TBD (testar após criação)
🚀 Desbloqueia: US-041 (error handling), US-042 (persist progress), L0-04 (treinamento)