Updated Jul 17, 2026

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 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, since an empty array [] is truthy, checking .length lets you properly fall back to seed data

Key Takeaways

  • Extract reusable components to $lib/components/ for cleaner code
  • Use browser from $app/environment to guard localStorage access during SSR
  • +page.server.ts runs only on the server, its return value becomes data in the page
  • Combine server-loaded seed data with client-side localStorage for persistence
  • Check stored?.length not just stored when falling back, since empty arrays are truthy
  • Component composition keeps each file focused on one responsibility

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy