March 26, 20269 min read

Svelte for React Developers: Everything That's Different and Why

A practical guide to Svelte for developers who already know React — what changes, what's simpler, what's harder, and whether the switch is worth it.

svelte react javascript frontend web-development
Ad 336x280

If you've been writing React for a while, you've internalized certain truths: state is immutable, effects need dependency arrays, the virtual DOM diffs and patches, hooks have rules, and everything is a function that returns JSX. Svelte looks at all of these "truths" and says: what if none of that was necessary?

Svelte is a compiler, not a runtime framework. It takes your component code and compiles it into efficient imperative JavaScript that directly manipulates the DOM. No virtual DOM. No diffing algorithm. No runtime library shipped to the browser. The result is smaller bundles, faster updates, and code that feels like you're just writing JavaScript, HTML, and CSS — because you mostly are.

The Fundamental Difference: Compiler vs Runtime

React ships a runtime (~45KB gzipped for React + ReactDOM) to every user's browser. This runtime maintains a virtual DOM tree, handles reconciliation, manages fiber scheduling, and coordinates updates. It's impressive engineering, but it's overhead.

Svelte ships no runtime. The compiler analyzes your components at build time and generates vanilla JavaScript that knows exactly how to update the DOM when state changes. A Svelte app that renders "Hello, World" ships essentially zero framework code.

This isn't just a bundle size optimization. It changes how you think about reactivity.

In React, you tell the framework "here's my new state" and the framework figures out what DOM changes are needed by comparing the old virtual DOM tree to the new one.

In Svelte, the compiler sees your state assignments and generates code that directly updates the specific DOM nodes that depend on that state. No comparison needed. No tree walking. Just element.textContent = newValue.

State: The Biggest Mindset Shift

In React, state is special. You declare it with useState, update it with setter functions, and if you forget the setter and mutate directly, nothing happens.

// React state
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

In Svelte 5 (the current version), state uses "runes" — special compiler hints that signal reactivity:

<!-- Svelte 5 state with runes -->
<script>
  let count = $state(0);
</script>

<button onclick={() => count++}>
Count: {count}
</button>

Notice what's different. There's no setter function. You just assign to the variable. count++ works. count = count + 1 works. The compiler sees the $state rune and generates code to update the DOM whenever count is reassigned.

This extends to objects and arrays:

<script>
  let todos = $state([
    { text: 'Learn Svelte', done: false },
    { text: 'Build something', done: false },
  ]);

function addTodo(text) {
// Direct mutation works — Svelte tracks it
todos.push({ text, done: false });
}

function toggleTodo(index) {
// This just works
todos[index].done = !todos[index].done;
}
</script>

In React, you'd need setTodos([...todos, newTodo]) or use Immer. In Svelte, you push to the array and the UI updates. The compiler generates proxy-based tracking that detects mutations.

Derived State and Effects

React's useMemo and useEffect have dependency arrays that you must keep correct. Get them wrong and you get stale closures or infinite loops.

// React derived state and effects
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

// Must specify [userId] or it won't re-run
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);

// Must specify [user] or it won't recompute
const displayName = useMemo(() => {
return user ? ${user.first} ${user.last} : 'Loading...';
}, [user]);

return <h1>{displayName}</h1>;
}

Svelte figures out dependencies automatically:

<script>
  let { userId } = $props();
  let user = $state(null);

// $effect automatically tracks which reactive values are read
$effect(() => {
fetchUser(userId).then(u => user = u);
});

// $derived automatically recomputes when dependencies change
let displayName = $derived(user ? ${user.first} ${user.last} : 'Loading...');
</script>

<h1>{displayName}</h1>

No dependency arrays. The compiler traces which reactive values are read inside $effect and $derived and re-runs them when those values change. This eliminates an entire category of bugs.

Component Structure

A React component is a JavaScript function that returns JSX. Styles are typically handled separately (CSS modules, styled-components, Tailwind).

A Svelte component is a .svelte file with three sections:

<!-- UserCard.svelte -->
<script>
  let { name, email, role = 'user' } = $props();
  let showEmail = $state(false);
</script>

<div class="card">
<h2>{name}</h2>
{#if showEmail}
<p class="email">{email}</p>
{/if}
<button onclick={() => showEmail = !showEmail}>
{showEmail ? 'Hide' : 'Show'} Email
</button>
<span class="badge" class:admin={role === 'admin'}>
{role}
</span>
</div>

<style>
.card {
padding: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
}
.email {
color: #666;
}
.badge {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 4px;
background: #e2e8f0;
}
.badge.admin {
background: #fed7aa;
color: #c2410c;
}
</style>

The