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 todo object is passed down for display, onremove is 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:

  • browser from $app/environment guards against running localStorage during SSR
  • $effect automatically tracks todos and re-runs whenever it changes, saving to localStorage
  • On load, todos initializes 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.ts always runs on the server (never shipped to the browser)
  • The load function's return value becomes data in 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.todos as seed data
  • Use stored?.length to check — an empty array [] is truthy, so check .length to properly fall back to seed data