CRUD with localStorage
CRUD on a JSON-encoded list stored in a single localStorage key - including try/catch on parse and versioned keys to survive schema changes between app deploys.
CRUD stands for Create, Read, Update, Delete - the four operations every persistent data store supports. With localStorage you can back a small list with a single JSON-encoded key: load it into memory, mutate the in-memory array, then write it back. The pattern is simple but requires careful error handling because JSON.parse throws when the stored data doesn't match what you expect.
Reading (the R in CRUD) means loading the JSON string from storage and parsing it. When the key doesn't exist yet getItem returns null, so you default to an empty array. Wrap JSON.parse in try/catch so a corrupted or schema-mismatched value doesn't crash the app.
const STORAGE_KEY = "tasks-v1";
function loadTasks() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
return JSON.parse(raw);
} catch {
// Stored data is not valid JSON or doesn't match the expected shape.
// Return an empty list rather than crashing.
console.warn("Could not parse stored tasks; starting fresh.");
return [];
}
}
function saveTasks(tasks) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
const tasks = loadTasks();
console.log(tasks); // [] on first runCreating a task means pushing a new object onto the array and saving. Updating a task means mapping over the array, replacing the item whose id matches, and saving. Both operations work on the in-memory copy and then persist it.
function createTask(text) {
const tasks = loadTasks();
const newTask = {
id: crypto.randomUUID(), // unique ID that won't collide
text,
done: false,
createdAt: new Date().toISOString(),
};
tasks.push(newTask);
saveTasks(tasks);
return newTask;
}
function updateTask(id, patch) {
const tasks = loadTasks();
const updated = tasks.map((task) =>
task.id === id ? { ...task, ...patch } : task
);
saveTasks(updated);
}
// Usage
const task = createTask("Buy groceries");
console.log(task.id); // "550e8400-e29b-41d4-a716-446655440000" (random UUID)
updateTask(task.id, { done: true });
console.log(loadTasks().find((t) => t.id === task.id)?.done); // trueDeleting a task means filtering it out of the array and saving the result. Clearing everything means writing an empty array (or removing the key entirely). The in-memory array never mutates in place - you always produce a new array and save it.
function deleteTask(id) {
const tasks = loadTasks();
const remaining = tasks.filter((task) => task.id !== id);
saveTasks(remaining);
}
function clearAllTasks() {
localStorage.removeItem(STORAGE_KEY);
}
// Usage
const t = createTask("Write tests");
console.log(loadTasks().length); // 1
deleteTask(t.id);
console.log(loadTasks().length); // 0
// Add a few, then wipe everything
createTask("Alpha");
createTask("Beta");
clearAllTasks();
console.log(loadTasks().length); // 0In production
JSON.parse on stored data can throw if a previous version of the app wrote a different shape - a renamed field, a changed type, a removed key. Always wrap it in try/catch and version your storage key (tasks-v1, tasks-v2) so a schema change starts fresh rather than corrupting every existing user's data. Two tabs open to the same page can clobber each other's writes because each tab loads, mutates, and saves without knowing about the other; for anything that needs concurrent writes, use IndexedDB or a server-side store instead.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free