Todo Next Steps Guide
📋 Jump to Takeaways🎁 Your todo app works, but everything lives in one file and forgets its data on refresh. What would it take to make it modular and persistent?
Take your time to try on your own before going through this one.
Step 1: Extract TodoItem Component
Create src/lib/components/TodoItem.svelte:
<script lang="ts">
let { todo, onremove }: { todo: { text: string; done: boolean }; onremove: () => void } = $props();
</script>
<li class="flex items-center gap-3 rounded border p-3">
<input type="checkbox" bind:checked={todo.done} class="h-4 w-4" />
<span class={todo.done ? 'flex-1 text-gray-400 line-through' : 'flex-1'}>
{todo.text}
</span>
<button onclick={onremove} class="text-red-500 hover:text-red-700" aria-label="Remove todo">
✕
</button>
</li>Update the {#each} block in src/routes/+page.svelte:
<script lang="ts">
import TodoItem from '$lib/components/TodoItem.svelte';
let todos = $state<{ id: number; text: string; done: boolean }[]>([]);
let newTodo = $state('');
function addTodo() {
const text = newTodo.trim();
if (!text) return;
todos.push({ id: Date.now(), text, done: false });
newTodo = '';
}
function removeTodo(id: number) {
todos = todos.filter((t) => t.id !== id);
}
</script>
<ul class="space-y-2">
{#each todos as todo (todo.id)}
<TodoItem {todo} onremove={() => removeTodo(todo.id)} />
{/each}
</ul>Key concepts:
$props()is how child components receive data from parents in Svelte 5- The
todoobject is passed down for display;onremoveis a callback so the child can trigger deletion without owning the state
Step 2: Add localStorage Persistence
Update the <script> block in src/routes/+page.svelte:
<script lang="ts">
import TodoItem from '$lib/components/TodoItem.svelte';
import { browser } from '$app/environment';
let todos = $state<{ id: number; text: string; done: boolean }[]>(
browser ? JSON.parse(localStorage.getItem('todos') ?? '[]') : []
);
let newTodo = $state('');
$effect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
});
function addTodo() {
const text = newTodo.trim();
if (!text) return;
todos.push({ id: Date.now(), text, done: false });
newTodo = '';
}
function removeTodo(id: number) {
todos = todos.filter((t) => t.id !== id);
}
</script>Key concepts:
browserfrom$app/environmentguards against runninglocalStorageduring SSR$effectautomatically trackstodosand re-runs whenever it changes, saving to localStorage- On load,
todosinitializes from localStorage if in the browser, otherwise starts empty
Step 3: Server-Side Data Loading
Create src/routes/+page.server.ts to provide seed todos from the server:
export function load() {
return {
todos: [
{ id: 1, text: 'Learn SvelteKit', done: false },
{ id: 2, text: 'Build something', done: false }
]
};
}Update src/routes/+page.svelte to use server data as a fallback:
<script lang="ts">
import TodoItem from '$lib/components/TodoItem.svelte';
import { browser } from '$app/environment';
let { data } = $props();
let stored = browser ? JSON.parse(localStorage.getItem('todos') ?? 'null') : null;
let todos = $state<{ id: number; text: string; done: boolean }[]>(
stored?.length ? stored : data.todos
);
let newTodo = $state('');
$effect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
});
function addTodo() {
const text = newTodo.trim();
if (!text) return;
todos.push({ id: Date.now(), text, done: false });
newTodo = '';
}
function removeTodo(id: number) {
todos = todos.filter((t) => t.id !== id);
}
</script>Key concepts:
+page.server.tsalways runs on the server (never shipped to the browser)- The
loadfunction's return value becomesdatain the page via$props() - This is where you'd put database queries, API calls with secret keys, etc.
- The logic: if localStorage has saved todos, use those. If not (first visit), fall back to server-provided
data.todosas seed data - Use
stored?.lengthto check, since an empty array[]is truthy, checking.lengthlets you properly fall back to seed data
Key Takeaways
- Extract reusable components to
$lib/components/for cleaner code - Use
browserfrom$app/environmentto guard localStorage access during SSR +page.server.tsruns only on the server, its return value becomesdatain the page- Combine server-loaded seed data with client-side localStorage for persistence
- Check
stored?.lengthnot juststoredwhen falling back, since empty arrays are truthy - Component composition keeps each file focused on one responsibility