Same vs Different: Reference Table
This page is your cheat sheet. Every major React concept maps to a Svelte equivalent — many are nearly identical in intent, different only in syntax. A handful require a genuine mental-model shift. Both kinds are listed here.
Full concept mapping
Section titled “Full concept mapping”| React | Svelte 5 | Notes |
|---|---|---|
function Component() | .svelte file | The file is the component — no function wrapper |
JSX return (...) | Markup body | No return, no parens — the template is the file body |
export default function | File is the export | Nothing to export manually |
className="foo" | class="foo" | Real HTML attributes — no renaming |
Function prop arguments ({ name }) | let { name } = $props() | Rune-based prop declaration |
useState(init) | $state(init) | Direct mutation replaces setter functions |
useMemo(() => x, [deps]) | $derived(x) | Auto-tracked, no dependency array |
useEffect(() => {}, [deps]) | $effect(() => {}) | Auto-tracked, cleanup via return function |
useCallback(fn, [deps]) | Plain function (no equivalent needed) | Functions are not re-created on each update |
useRef(null) | let el = $state(null) / bind:this={el} | bind:this binds to the DOM element |
useContext / createContext | Svelte stores / setContext / getContext | Context API exists; stores preferred for reactivity |
children prop | {@render children()} snippet | Snippets are typed template fragments, not values |
| Named slot / render prop | {#snippet name()} / {@render name()} | More explicit and type-safe than render props |
onClick | onclick | Lowercase, plain HTML event attributes |
onChange | oninput | oninput fires on every keystroke; onchange fires on blur |
onKeyDown | onkeydown | All event names are lowercase in Svelte 5 |
<input value={x} onChange={fn} | bind:value={x} | Two-way binding replaces the controlled-input pattern |
| CSS Modules / styled-components | <style> block | Auto-scoped, zero config |
clsx(a, { b: cond }) | class:b={cond} | Built-in conditional class directive |
React Router / next/link | SvelteKit <a href> / goto() | SvelteKit handles routing; plain anchors work |
useNavigate | import { goto } from '$app/navigation' | SvelteKit navigation API |
useSearchParams | import { page } from '$app/stores' | $page.url.searchParams |
<Suspense> / lazy | {#await} block | Built into the template language |
React.memo | Not needed | Svelte does not re-run the whole component on updates |
| Create React App | Vite (+ @sveltejs/vite-plugin-svelte) | CRA is deprecated; Vite is the standard |
| Next.js | SvelteKit | Full-stack framework: SSR, routing, API routes |
The biggest surprises
Section titled “The biggest surprises”1. Mutation is fine
Section titled “1. Mutation is fine”// React — must be immutableconst [items, setItems] = React.useState([]);function addItem(item) { setItems([...items, item]); // spread, never push}<script> // Svelte — direct mutation is tracked let items = $state([]); function addItem(item) { items.push(item); // push is fine — Svelte detects it }</script>2. No hook rules
Section titled “2. No hook rules”React hooks have strict rules: call at the top level, never inside conditions or loops, always in the same order. Svelte runes have none of these restrictions — you can call $state inside a condition or inside a loop. They are compiler directives, not function calls tracked by call order.
// React — hooks must be at the top level, always calledfunction Conditional({ show }) { // RULE: can't put hooks inside an if — even though it's unused const [x, setX] = React.useState(0); if (!show) return null; return <button onClick={() => setX(x + 1)}>{x}</button>;}<script> let { show } = $props(); // Runes can be declared anywhere — no ordering rules let x = $state(0);</script>
{#if show} <button onclick={() => x++}>{x}</button>{/if}