Todo Next Steps Guide
Take your time to try on your own before going thourgh 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 — an empty array[]is truthy, so check.lengthto properly fall back to seed data