Chapter 3: Svelte - The Compiler Will See You Now

What If Your Framework Just… Disappeared?

Pop quiz: What’s better than a fast framework?

No framework.

“But wait,” your React brain protests, “we NEED a framework! For reactivity! For component lifecycle! For—”

What if I told you the framework could do its job at build time and then get out of the way?

That’s Svelte. It’s not a framework you ship to your users. It’s a compiler that turns your components into highly optimized vanilla JavaScript. The framework disappears. Poof. Gone. Like it was never there.

Your bundle? Tiny. Your runtime? Fast. Your mind? Blown.

No Virtual DOM, No Problem

Remember when React explained the virtual DOM?

“We create a virtual representation of the DOM in JavaScript, then we diff it with the previous version, then we calculate the minimal set of changes, then we batch them, then we update the real DOM.”

And you nodded along, thinking this sounded reasonable.

It’s not. It’s a workaround.

Svelte looked at this and said “what if we just… knew exactly what changed?”

<script>
  let count = 0
</script>

<button on:click={() => count++}>
  Clicks: {count}
</button>

When you click the button, Svelte doesn’t:

Svelte just updates the text node. That’s it. Because the compiler looked at your code and said “oh, when count changes, update this specific text node” and generated code to do exactly that.

No runtime overhead. No reconciliation. No “React is working” message in DevTools.

Just surgical DOM updates.

Reactivity Without the Ceremony

React’s reactivity:

const [count, setCount] = useState(0)
const [doubled, setDoubled] = useState(0)

useEffect(() => {
  setDoubled(count * 2)
}, [count])

// Or with useMemo
const doubled = useMemo(() => count * 2, [count])

“Don’t forget the dependency array! And yes, you need a whole different hook for computed values. Why? Because that’s just how it works. Stop asking questions.”

Svelte’s reactivity:

<script>
  let count = 0
  $: doubled = count * 2
</script>

That $: is a label. A JavaScript label. Svelte’s compiler sees it and makes it reactive. When count changes, doubled updates. No hooks. No arrays. No rules.

“But that’s using a weird JavaScript feature!”

Yes. A feature that’s been in JavaScript since 1995. Svelte just made it useful.

Reactive Statements

You can make any statement reactive:

<script>
  let firstName = ''
  let lastName = ''

  $: fullName = `${firstName} ${lastName}`
  $: console.log('Full name changed:', fullName)

  $: if (fullName.length > 20) {
    console.log('That is a long name!')
  }
</script>

When firstName or lastName change, fullName recalculates, the log runs, and the conditional checks. The compiler figures out the dependencies automatically.

No hooks. No dependency arrays. No useEffect. No crying.

The Component Model

React component:

function Counter({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount)

  const increment = useCallback(() => {
    setCount(c => c + 1)
  }, [])

  useEffect(() => {
    console.log('Count changed:', count)
  }, [count])

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
    </div>
  )
}

Svelte component:

<script>
  export let initialCount = 0

  let count = initialCount

  function increment() {
    count++
  }

  $: console.log('Count changed:', count)
</script>

<p>Count: {count}</p>
<button on:click={increment}>+1</button>

Same functionality. Way less code. No hooks. No useCallback. No function-that-returns-JSX mental model.

Just: script, markup, done.

Props

React:

// Props are function parameters
// Optional props need default values
// TypeScript types are separate
// Destructuring is common but breaks default values sometimes

function Button({
  variant = 'primary',
  disabled = false,
  onClick,
  children
}) {
  // ...
}

Svelte:

<script lang="ts">
  // Props are exported variables
  export let variant: 'primary' | 'secondary' = 'primary'
  export let disabled = false
  export let onClick: () => void
</script>

<button class={variant} {disabled} on:click={onClick}>
  <slot />
</button>

Props are just variables you export. Defaults work like normal variable defaults. TypeScript integration is seamless. Slots are like children but better (more on that later).

Stores: State Management That Doesn’t Require a Support Group

React + Context + useReducer:

const CounterContext = React.createContext()

function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      return state
  }
}

function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 })
  return (
    <CounterContext.Provider value=>
      {children}
    </CounterContext.Provider>
  )
}

function Counter() {
  const { state, dispatch } = useContext(CounterContext)
  return (
    <button onClick={() => dispatch({ type: 'increment' })}>
      {state.count}
    </button>
  )
}

Svelte stores:

// stores.js
import { writable } from 'svelte/store'

export const count = writable(0)
<!-- Counter.svelte -->
<script>
  import { count } from './stores.js'
</script>

<button on:click={() => $count++}>
  {$count}
</button>

That $ prefix automatically subscribes to the store, gets its value, and unsubscribes when the component unmounts. No Provider. No Context. No hooks. Just a dollar sign.

Derived Stores

import { writable, derived } from 'svelte/store'

export const count = writable(0)
export const doubled = derived(count, $count => $count * 2)
<script>
  import { count, doubled } from './stores.js'
</script>

<p>Count: {$count}</p>
<p>Doubled: {$doubled}</p>

When count changes, doubled updates automatically. It’s like useMemo but you don’t have to wrap everything in hooks or remember dependency arrays.

Custom Stores

You can make stores with custom logic:

function createCounter() {
  const { subscribe, update } = writable(0)

  return {
    subscribe,
    increment: () => update(n => n + 1),
    decrement: () => update(n => n - 1),
    reset: () => update(() => 0)
  }
}

export const counter = createCounter()
<script>
  import { counter } from './stores.js'
</script>

<button on:click={counter.decrement}>-</button>
<span>{$counter}</span>
<button on:click={counter.increment}>+</button>
<button on:click={counter.reset}>Reset</button>

No classes. No Redux. No ceremony. Just functions that return objects with a subscribe method.

Transitions & Animations: The Good Kind of Magic

React animations:

// Install react-spring
// Or framer-motion
// Or react-transition-group
// Read 50 pages of docs
// Fight with CSS
// Give up
// Use setTimeout like an animal

Svelte animations:

<script>
  import { fade, fly, slide } from 'svelte/transition'

  let visible = true
</script>

<button on:click={() => visible = !visible}>
  Toggle
</button>

{#if visible}
  <p transition:fade>Fades in and out</p>
  <p transition:fly=>Flies in and out</p>
  <p transition:slide>Slides in and out</p>
{/if}

Built in. Works perfectly. No dependencies. No fighting with CSS. Just import, use, done.

List Animations

<script>
  import { flip } from 'svelte/animate'
  import { fade } from 'svelte/transition'

  let items = [1, 2, 3, 4, 5]

  function shuffle() {
    items = items.sort(() => Math.random() - 0.5)
  }
</script>

<button on:click={shuffle}>Shuffle</button>

{#each items as item (item)}
  <div animate:flip transition:fade>
    {item}
  </div>
{/each}

The animate:flip smoothly animates items when the list order changes. The transition:fade handles enter/exit. That’s it.

Try doing that in React without a library. Go ahead. I’ll wait.

Migration Strategies: From React to Svelte

Strategy 1: The Component Swap

Start by converting leaf components—things like buttons, inputs, cards:

Before (React):

export function Card({ title, children, className }) {
  return (
    <div className={`card ${className || ''}`}>
      <h3>{title}</h3>
      <div className="card-body">
        {children}
      </div>
    </div>
  )
}

After (Svelte):

<script>
  export let title
  export let className = ''
</script>

<div class="card {className}">
  <h3>{title}</h3>
  <div class="card-body">
    <slot />
  </div>
</div>

Use them together with a bridge:

// ReactSveltebridge.jsx
import { mount } from 'svelte'

export function useSvelteComponent(Component, props) {
  const ref = useRef(null)

  useEffect(() => {
    if (ref.current) {
      const component = mount(Component, {
        target: ref.current,
        props
      })
      return () => component.$destroy()
    }
  }, [props])

  return ref
}

Strategy 2: New Features in Svelte

Like with Vue, you can write new features in Svelte while keeping old ones in React. SvelteKit plays nice with existing backends.

Strategy 3: The Page-by-Page Rewrite

Move entire pages to SvelteKit:

// routes/dashboard/+page.svelte
<script>
  // SvelteKit page
</script>

// routes/old-dashboard/+page.jsx
// Still using React

Converting Hooks to Svelte Patterns

useState:

const [count, setCount] = useState(0)
let count = 0

useEffect:

useEffect(() => {
  console.log('count changed')
}, [count])
$: console.log('count changed', count)

useMemo:

const doubled = useMemo(() => count * 2, [count])
$: doubled = count * 2

useCallback:

const increment = useCallback(() => {
  setCount(c => c + 1)
}, [])
function increment() {
  count++
}

Notice a pattern? Everything gets simpler.

The Bundle Size Moment

Remember earlier when I mentioned bundle size? Let’s talk numbers.

A simple counter app:

A real app (e-commerce site):

That’s not a typo. The Svelte version shipped 1/8th the JavaScript. Same features. Same functionality. Just 335KB less framework overhead.

Your users’ phones will thank you.

When to Use Svelte

Svelte is phenomenal for:

Svelte might not be ideal for:

The Aha Moment

You’ll know Svelte has clicked when:

  1. You forget the framework exists and just write code
  2. You check your bundle size and laugh
  3. You add an animation without installing anything
  4. You realize you haven’t thought about re-renders in days
  5. You catch yourself saying “it just works” unironically

The Trade-offs

You lose:

You gain:

Real Talk: Svelte vs React

React asks: “How do we make JavaScript apps manageable at Facebook scale?” Svelte asks: “How do we make building websites delightful?”

Different questions. Different answers.

If you’re building Facebook, use React. If you’re building literally anything else, maybe try the one that prioritizes your users and your sanity.


“I thought the virtual DOM was necessary. Turns out it was just all I knew.” — A Developer Who Checked Their Bundle Size

Up Next: Chapter 4: Solid - React’s Cooler Younger Sibling