完成図
miseでBunセットアップ
$ mise use -g bun@1.1.42
$ bun -v
Todoプロジェクトの作成
$ bunx sv create svelte-todo-app
┌ Welcome to the Svelte CLI! (v0.6.10)
│
◆ Which template would you like?
│ # プロジェクトテンプレートをどうするか。今回は minimal を選択する
│ ● SvelteKit minimal (barebones scaffolding for your new app)
│ ○ SvelteKit demo
│ ○ Svelte library
│
◇ Add type checking with Typescript?
│ # TypeScriptを使うか? 特に使わない選択肢はないと思うのでYes
│ ● Yes, using Typescript syntax
│ ○ Yes, using Javascript with JSDoc comments
│ ○ No
│
◇ What would you like to add to your project? (use arrow
keys / space bar)
| # プロジェクトに追加したいツールはあるか
| # デモアプリに必要ないと思うが、コードフォーマッターと静的解析ツールを選択
│ ◼ prettier (formatter - https://prettier.io) # コードフォーマッター
│ ◼ eslint # JavaScript/TypeScript用の静的解析ツール
│ ◻ vitest # ユニットテストフレームワーク
│ ◻ playwright # エンドツーエンド(E2E)テスト用フレームワーク
│ ◻ tailwindcss # CSSフレームワーク
│ ◻ sveltekit-adapter # SvelteKitプロジェクトのビルド先を指定するためのアダプター
│ ◻ drizzle # TypeScript対応の軽量ORM
│ ◻ lucia # 認証ライブラリ
│ ◻ mdsvex # MarkdownとSvelteを統合するためのプラグイン
│ ◻ paraglide # Svelteアプリ用の国際化(i18n)ライブラリ
│ ◻ storybook # UIコンポーネントを個別に開発・テストするためのツール
│
◆ Which package manager do you want to install dependencies
with?
| # パッケージマネージャを選択。今回はbunを使用する
│ ○ None
│ ○ npm
│ ○ yarn
│ ○ pnpm
│ ● bun
│ ○ deno
│
◆ Successfully setup add-ons
│
◆ Successfully installed dependencies
│
◇ Successfully formatted modified files
│
◇ Project next steps ─────────────────────────────────────────────────────╮
│ │
│ 1: cd svelte-todo-app │
│ 2: git init && git add -A && git commit -m "Initial commit" (optional) │
│ 3: bun run dev --open │
│ │
│ To close the dev server, hit Ctrl-C │
│ │
│ Stuck? Visit us at https://svelte.dev/chat │
│ │
├──────────────────────────────────────────────────────────────────────────╯
│
└ You're all set!
SvelteKitを起動する
$ cd svelte-todo-app
$ git init && git add -A && git commit -m "Initial commit"
$ bun dev --open
インデントをタブからスペースに変更
個人の好みですが、インデントはスペース派なので変更してます。
{
- "useTabs": true,
+ "useTabs": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
$ bun run format
便利コマンド一覧
$ bun run dev # Viteのローカル開発用サーバーを起動。自動リロード機能
$ bun run build # プロジェクトを本番用にビルド。distディレクトリにビルド成果物が出力
$ bun run preview # ビルド済みプロジェクトのプレビュー
$ bun run check # プロジェクトの型や設定の整合性をチェック
$ bun run check:watch # 型チェックの監視モードで実行
$ bun run format # コードフォーマットを実行
$ bun run lint # コードの静的解析とフォーマットチェックを実行
シンプルなTodoアプリを作る
src/routes/+page.svelte
ファイルに次のコードを記述します。
<script lang="ts">
type Todo = {
text: string;
done: boolean;
};
let todos: Todo[] = $state([
{ text: 'Apple', done: false },
{ text: 'パン', done: false },
{ text: '牛乳', done: true },
{ text: 'Coffee', done: false }
]);
let newTodo: string = $state('');
function resetInput() {
newTodo = '';
}
function addTodo() {
if (newTodo.trim()) {
todos.push({ text: newTodo, done: false });
resetInput();
}
}
function deleteTodo(index: number) {
todos.splice(index, 1);
}
</script>
<main>
<h1>Todo App</h1>
<input
type="text"
bind:value={newTodo}
placeholder="New task..."
onkeydown={(e) => {
if (e.key === 'Enter' && !e.isComposing) {
addTodo();
}
}}
/>
<button onclick={addTodo} disabled={!newTodo.trim()}>Add</button>
<ul>
{#each todos as todo, i}
<li>
<input type="checkbox" bind:checked={todo.done} />
<span class={todo.done ? 'done' : ''}>{todo.text}</span>
<button onclick={() => deleteTodo(i)}>Delete</button>
</li>
{/each}
</ul>
</main>
<style>
span.done {
text-decoration: line-through;
}
button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
</style>
Todo型の切り出し
Todo型を別ファイルに切り出して再利用可能にすることで、将来的な拡張性を高められます。
src/types/todo.ts
と新しく型ファイルを作成してインポートしてみます。
export type Todo = {
text: string;
done: boolean;
};
<script lang="ts">
- type Todo = {
- text: string;
- done: boolean;
- };
+ import type { Todo } from '../types/todo';
...
データの永続化
現状ではTodoリストがメモリ内にのみ保持され、ページをリロードするとTodoリストがリセットされます。
ブラウザのlocalStorageを使ってデータを永続化します。
<script lang="ts">
import type { Todo } from '../types/todo';
+ import { onMount } from 'svelte';
- let todos: Todo[] = $state([
- { text: 'Apple', done: false },
- { text: 'パン', done: false },
- { text: '牛乳', done: true },
- { text: 'Coffee', done: false }
- ]);
+ let todos = $state([] as Todo[]);
let newTodo: string = $state('');
+ let isInitialized: boolean = $state(false);
+
+ onMount(() => {
+ if (typeof window !== 'undefined') {
+ try {
+ const savedTodos = localStorage.getItem('todos');
+ if (savedTodos) {
+ todos = JSON.parse(savedTodos);
+ }
+ } catch (e) {
+ console.error('Failed to load todos from localStorage:', e);
+ } finally {
+ isInitialized = true;
+ }
+ }
+ });
+
+ $effect(() => {
+ if (isInitialized && typeof window !== 'undefined') {
+ try {
+ localStorage.setItem('todos', JSON.stringify(todos));
+ } catch (e) {
+ console.error('Failed to save todos to localStorage:', e);
+ }
+ }
+ });
function resetInput() {
newTodo = '';
}
- onMountを使い、localStorageからデータを復元
- $stateでtodos、newTodo、isInitializedをリアクティブな状態として管理
- $effectを利用して、todosが変更されるたびにlocalStorageに保存
- 初期化完了(isInitializedがtrue)後にのみ保存処理を実行
- ハードコードな初期値を削除
スタイルの強化
Tailwind CSSフレームワークを導入してTodoアプリの見た目を少しオシャレにします。
$ bun install -d tailwindcss postcss autoprefixer
$ bunx tailwindcss init -p
$ bun run format
svelte.config.js
は特に変更ありません。
次のデフォルトの内容です。
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;
tailwind.config.js
を修正します。
/** @type {import('tailwindcss').Config} */
export default {
- content: [],
+ content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: []
};
src/app.css
を新たに追加します。
@tailwind base;
@tailwind components;
@tailwind utilities;
src/routes/+layout.svelte
を新たに追加します。
<script>
import '../app.css';
</script>
<slot />
開発サーバーを再起動します。
$ bun dev --open
補足: 開発サーバーでエラー
$ bun dev --open
failed to load config from /Users/your-name/svelte-todo-app/vite.config.ts
error when starting dev server:
Error: The service was stopped
at /Users/your-name/svelte-todo-app/node_modules/esbuild/lib/main.js:968:34
at responseCallbacks.<computed> (/Users/your-name/svelte-todo-app/node_modules/esbuild/lib/main.js:622:9)
at Socket.afterClose (/Users/your-name/svelte-todo-app/node_modules/esbuild/lib/main.js:613:28)
at Socket.emit (node:events:519:35)
at endReadableNT (node:internal/streams/readable:1696:12)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21)
error: script "dev" exited with code 1
私が試した時、 node_modules
ファイルが壊れたようでこのエラーが発生しました。
$ rm -rf node_modules
$ bun install
$ bun dev --open
上記のコマンドでエラーが解消されました。
<script lang="ts">
import type { Todo } from '../types/todo';
import { onMount } from 'svelte';
let todos = $state([] as Todo[]);
let newTodo: string = $state('');
let isInitialized: boolean = $state(false);
onMount(() => {
if (typeof window !== 'undefined') {
try {
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
todos = JSON.parse(savedTodos);
}
} catch (e) {
console.error('Failed to load todos from localStorage:', e);
} finally {
isInitialized = true;
}
}
});
$effect(() => {
if (isInitialized && typeof window !== 'undefined') {
try {
localStorage.setItem('todos', JSON.stringify(todos));
} catch (e) {
console.error('Failed to save todos to localStorage:', e);
}
}
});
function resetInput() {
newTodo = '';
}
function addTodo() {
if (newTodo.trim()) {
todos.push({ text: newTodo, done: false });
resetInput();
}
}
function deleteTodo(index: number) {
todos.splice(index, 1);
}
</script>
<main class="min-h-screen bg-gray-100 flex flex-col items-center py-10">
<h1 class="text-4xl font-bold text-gray-800 mb-8">Todo App</h1>
<div class="flex items-center gap-4 mb-6">
<input
type="text"
bind:value={newTodo}
placeholder="New task..."
onkeydown={(e) => {
if (e.key === 'Enter' && !e.isComposing) {
addTodo();
}
}}
class="w-64 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onclick={addTodo}
disabled={!newTodo.trim()}
class="px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Add
</button>
</div>
<ul class="w-full max-w-md">
{#each todos as todo, i}
<li class="flex items-center justify-between bg-white p-4 rounded-lg shadow-sm mb-2">
<div class="flex items-center gap-3">
<input
type="checkbox"
bind:checked={todo.done}
class="h-5 w-5 text-blue-500 focus:ring-blue-500 border-gray-300 rounded"
/>
<span class={`text-lg ${todo.done ? 'line-through text-gray-400' : 'text-gray-700'}`}
>{todo.text}</span
>
</div>
<button onclick={() => deleteTodo(i)} class="text-red-500 font-semibold hover:underline">
Delete
</button>
</li>
{/each}
</ul>
</main>
見た目が変わるとテンション上がりますね!
こんな感じにSvelteでTodoアプリを作ってみました。
GitHubに今回のTodoアプリのコードを置いてます。