A comprehensive learning platform for React developers to understand and transition to Svelte 5 and SvelteKit. This application demonstrates the syntax differences between React and Svelte through interactive examples and side-by-side comparisons.
This project serves as a practical guide for React developers who want to learn Svelte. Each section of the dashboard showcases equivalent patterns in both frameworks, making the learning curve smoother.
# Install dependencies
pnpm install
# Start development server
pnpm run dev
# Build for production
pnpm run build
# Preview production build
pnpm run preview
src/
āāā lib/
ā āāā stores/ # Global state management (Svelte's alternative to Zustand/Redux)
ā ā āāā auth.svelte.ts
ā ā āāā global-counter.svelte.ts
ā āāā components/ # Reusable UI components
ā ā āāā CodeComparison.svelte
ā ā āāā KeyedInput.svelte
ā āāā schemas/ # Zod validation schemas
ā āāā login.ts
āāā routes/
ā āāā +layout.svelte # Root layout
ā āāā +page.svelte # Home/Login page
ā āāā dashboard/ # Protected dashboard routes
ā āāā +layout.svelte
ā āāā +page.svelte
ā āāā state/
ā āāā effects/
ā āāā memo/
ā āāā props/
ā āāā components/
ā āāā data-fetching/
ā āāā routing/
ā āāā async-ui/
ā āāā key-blocks/
ā āāā snippets/
ā āāā stores/
ā āāā debugging/
Note: this project enables Svelte's compilerOptions.experimental.async (in svelte.config.js) to demo await expressions and $state.eager.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
Count: {count}
</button>
Key Differences:
$state rune instead of useState hookonclick (native DOM events)import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => clearInterval(interval); // Cleanup
}, []); // Dependency array
return <div>Seconds: {seconds}</div>;
}
<script>
let seconds = $state(0);
$effect(() => {
const interval = setInterval(() => {
seconds++;
}, 1000);
return () => clearInterval(interval); // Cleanup
});
// No dependency array needed - Svelte auto-tracks!
</script>
<div>Seconds: {seconds}</div>
Key Differences:
$effect runs after DOM updates (like useEffect)$effect.pre for effects that run before DOM updatesimport { useState, useMemo } from 'react';
function ExpensiveCalculation() {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const [multiplier, setMultiplier] = useState(2);
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item * multiplier, 0);
}, [items, multiplier]); // Must specify dependencies
return <div>Total: {total}</div>;
}
<script>
let items = $state([1, 2, 3, 4, 5]);
let multiplier = $state(2);
// Simple expression
let total = $derived(items.reduce((sum, item) => sum + item * multiplier, 0));
// Complex logic with $derived.by
let analysis = $derived.by(() => {
const sum = items.reduce((a, b) => a + b, 0);
const avg = sum / items.length;
return { sum, avg, multiplied: sum * multiplier };
});
</script>
<div>Total: {total}</div><div>Average: {analysis.avg}</div>
Key Differences:
$derived auto-tracks dependencies like $effect$derived.by(() => ...) for complex multi-line computations// Child component
function Greeting({ name, age = 18, onGreet }) {
return (
<div>
<h1>
Hello, {name}! You are {age}.
</h1>
<button onClick={onGreet}>Greet</button>
</div>
);
}
// Parent component
function App() {
return <Greeting name="Alice" age={25} onGreet={() => alert('Hi!')} />;
}
<!-- Child: Greeting.svelte -->
<script>
let { name, age = 18, onGreet } = $props();
</script>
<div>
<h1>Hello, {name}! You are {age}.</h1>
<button onclick={onGreet}>Greet</button>
</div>
<!-- Parent: App.svelte -->
<script>
import Greeting from './Greeting.svelte';
</script>
<Greeting name="Alice" age={25} onGreet={() => alert('Hi!')} />
Key Differences:
$props() runelet { name }: { name: string } = $props()let { name, ...rest } = $props()import { useState } from 'react';
import './Card.css';
export default function Card({ title, children }) {
const [expanded, setExpanded] = useState(false);
return (
<div className={`card ${expanded ? 'expanded' : ''}`}>
<h2 onClick={() => setExpanded(!expanded)}>{title}</h2>
{expanded && <div className="content">{children}</div>}
</div>
);
}
<!-- Card.svelte -->
<script>
let { title, children } = $props();
let expanded = $state(false);
</script>
<div class="card" class:expanded>
<h2 onclick={() => (expanded = !expanded)}>{title}</h2>
{#if expanded}
<div class="content">{@render children()}</div>
{/if}
</div>
<style>
.card {
border: 1px solid #ccc;
padding: 1rem;
}
.expanded {
background: #f0f0f0;
}
</style>
Key Differences:
<script>, markup, and <style> sectionsclass:name directive for conditional classes{#if}...{/if} block syntax for conditionals{@render children()} instead of {children}className - use native classfunction Status({ isLoggedIn, isAdmin, items }) {
return (
<div>
{isLoggedIn ? <Dashboard /> : <Login />}
{isAdmin && <AdminPanel />}
{items.length > 0 ? (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
) : (
<p>No items found</p>
)}
</div>
);
}
<script>
let { isLoggedIn, isAdmin, items } = $props();
</script>
<div>
{#if isLoggedIn}
<Dashboard />
{:else}
<Login />
{/if}
{#if isAdmin}
<AdminPanel />
{/if}
{#if items.length > 0}
<ul>
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
</ul>
{:else}
<p>No items found</p>
{/if}
</div>
Key Differences:
{#if}, {:else if}, {:else}, {/if}{#each} for loops with built-in key syntax (item.id){:else} works with {#each} for empty states// store.js
import { create } from 'zustand';
export const useAuthStore = create((set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false })
}));
// Component.jsx
import { useAuthStore } from './store';
function Profile() {
const { user, logout } = useAuthStore();
return (
<div>
<p>Welcome, {user.name}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
// lib/stores/auth.svelte.ts
class AuthStore {
user = $state<User | null>(null);
isAuthenticated = $derived(this.user !== null);
login(user: User) {
this.user = user;
}
logout() {
this.user = null;
}
}
export const authStore = new AuthStore();
// Component.svelte
<script>
import { authStore } from '$lib/stores/auth.svelte';
</script>
<div>
<p>Welcome, {authStore.user.name}</p>
<button onclick={() => authStore.logout()}>Logout</button>
</div>
Key Differences:
.svelte.ts files to use runes outside components$state and $derived are powerfulimport { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm({
resolver: zodResolver(schema)
});
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Login</button>
</form>
);
}
// +page.server.ts
import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { z } from 'zod';
import { fail } from '@sveltejs/kit';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export const load = async () => {
const form = await superValidate(zod(schema));
return { form };
};
export const actions = {
default: async ({ request }) => {
const form = await superValidate(request, zod(schema));
if (!form.valid) return fail(400, { form });
// Handle login
return { form };
}
};
<!-- +page.svelte -->
<script lang="ts">
import { superForm } from 'sveltekit-superforms';
import { zodClient } from 'sveltekit-superforms/adapters';
let { data } = $props();
const { form, errors, enhance } = superForm(data.form, {
validators: zodClient(schema)
});
</script>
<form method="POST" use:enhance>
<input name="email" bind:value={$form.email} />
{#if $errors.email}<span>{$errors.email}</span>{/if}
<input type="password" name="password" bind:value={$form.password} />
{#if $errors.password}<span>{$errors.password}</span>{/if}
<button>Login</button>
</form>
Key Differences:
use:enhancebind:value for two-way bindingimport { useQuery } from '@tanstack/react-query';
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then((r) => r.json())
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
<script>
import { createQuery } from '@tanstack/svelte-query';
const query = createQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then((r) => r.json())
});
</script>
{#if $query.isLoading}
<div>Loading...</div>
{:else if $query.error}
<div>Error: {$query.error.message}</div>
{:else}
<ul>
{#each $query.data as user (user.id)}
<li>{user.name}</li>
{/each}
</ul>
{/if}
// +page.server.ts
export async function load({ fetch }) {
const users = await fetch('/api/users').then(r => r.json());
return { users };
}
<!-- +page.svelte -->
<script>
let { data } = $props();
</script>
<ul>
{#each data.users as user (user.id)}
<li>{user.name}</li>
{/each}
</ul>
Key Differences:
load functions for server-side data fetching$ prefiximport { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/users/123">User 123</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<User />} />
</Routes>
</BrowserRouter>
);
}
function User() {
const { id } = useParams();
const navigate = useNavigate();
return (
<div>
<h1>User {id}</h1>
<button onClick={() => navigate('/')}>Go Home</button>
</div>
);
}
src/routes/
āāā +page.svelte # /
āāā about/
ā āāā +page.svelte # /about
āāā users/
āāā [id]/
āāā +page.svelte # /users/:id
<!-- src/routes/+layout.svelte -->
<script>
let { children } = $props();
</script>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/users/123">User 123</a>
</nav>
{@render children()}
<!-- src/routes/users/[id]/+page.svelte -->
<script>
import { page } from '$app/state';
import { goto } from '$app/navigation';
</script>
<h1>User {page.params.id}</h1>
<button onclick={() => goto('/')}>Go Home</button>
Key Differences:
[param] folders for dynamic routes<a> tags instead of <Link> components$app/navigation for programmatic navigation$app/state for accessing route params and URL$state, $derived, $effect, $props, $bindableMIT