Skip to main content
Migrating from React to Svelte offers significant benefits: smaller bundle sizes, better performance, and less boilerplate. This guide will help you understand the differences and successfully migrate your React applications to Svelte.

Why Migrate to Svelte?

  • No Virtual DOM: Svelte compiles to efficient vanilla JavaScript
  • Less Boilerplate: Write less code to achieve the same functionality
  • Built-in Reactivity: No need for hooks or state management libraries
  • Smaller Bundles: Significantly smaller production builds
  • Better Performance: Faster runtime performance out of the box

Key Concept Mapping

React ConceptSvelte EquivalentNotes
JSXSvelte TemplatesNo JSX transform needed
useState$stateBuilt-in reactivity
useEffect$effectSimpler dependency tracking
useMemo$derivedAutomatic memoization
useCallbackRegular functionsNo need to memoize
useReflet + bind:thisSimpler binding syntax
useContextContext APISimilar, but simpler
useReducer$state + functionsState management built-in
Props$props()Destructured props
Childrenchildren propRendered with {@render}
Custom HooksRunesMore powerful primitives

Component Basics

Component Definition

import React from 'react';

function Welcome({ name }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
    </div>
  );
}

export default Welcome;

State Management

import { useState } from 'react';

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

Computed Values

import { useState, useMemo } from 'react';

function ShoppingCart({ items }) {
  const total = useMemo(() => {
    return items.reduce((sum, item) => sum + item.price, 0);
  }, [items]);
  
  return <div>Total: ${total}</div>;
}

Effects and Side Effects

Basic Effects

import { useEffect, useState } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
    
    return () => clearInterval(interval);
  }, []);
  
  return <div>Seconds: {seconds}</div>;
}

Watching Dependencies

import { useEffect, useState } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser);
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

Props and Component Communication

Props Passing

// Parent
function App() {
  return (
    <UserCard 
      name="Alice"
      age={30}
      email="alice@example.com"
    />
  );
}

// Child
function UserCard({ name, age, email }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Email: {email}</p>
    </div>
  );
}

Default Props

function Button({ text = 'Click me', variant = 'primary' }) {
  return <button className={variant}>{text}</button>;
}

Two-Way Binding

// Parent
function App() {
  const [value, setValue] = useState('');
  
  return <Input value={value} onChange={setValue} />;
}

// Child
function Input({ value, onChange }) {
  return (
    <input 
      value={value}
      onChange={(e) => onChange(e.target.value)}
    />
  );
}

Callbacks and Events

// Parent
function App() {
  const handleClick = (data) => {
    console.log('Clicked:', data);
  };
  
  return <CustomButton onClick={handleClick} />;
}

// Child
function CustomButton({ onClick }) {
  return (
    <button onClick={() => onClick({ id: 1 })}>
      Click me
    </button>
  );
}

Rendering Patterns

Conditional Rendering

function UserStatus({ isLoggedIn, user }) {
  if (isLoggedIn) {
    return <div>Welcome, {user.name}!</div>;
  } else {
    return <div>Please log in</div>;
  }
}

// Or with ternary
function UserStatus({ isLoggedIn, user }) {
  return (
    <div>
      {isLoggedIn ? (
        <div>Welcome, {user.name}!</div>
      ) : (
        <div>Please log in</div>
      )}
    </div>
  );
}

List Rendering

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

Children and Composition

// Container component
function Card({ children, title }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      {children}
    </div>
  );
}

// Usage
function App() {
  return (
    <Card title="My Card">
      <p>Card content goes here</p>
    </Card>
  );
}

Forms and Input Handling

Controlled Inputs

function Form() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  
  const handleChange = (field) => (e) => {
    setFormData(prev => ({
      ...prev,
      [field]: e.target.value
    }));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(formData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={formData.name}
        onChange={handleChange('name')}
      />
      <input 
        value={formData.email}
        onChange={handleChange('email')}
      />
      <textarea 
        value={formData.message}
        onChange={handleChange('message')}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Lifecycle and Refs

Component Lifecycle

import { useEffect } from 'react';

function Component() {
  useEffect(() => {
    console.log('Component mounted');
    
    return () => {
      console.log('Component unmounting');
    };
  }, []);
  
  return <div>Component</div>;
}

DOM Refs

import { useRef, useEffect } from 'react';

function AutoFocusInput() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
  
  return <input ref={inputRef} />;
}

Context API

import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedComponent />
    </ThemeContext.Provider>
  );
}

function ThemedComponent() {
  const theme = useContext(ThemeContext);
  return <div>Theme: {theme}</div>;
}

Advanced Patterns

Higher-Order Components vs Composition

// HOC pattern
function withAuth(Component) {
  return function AuthenticatedComponent(props) {
    const { isAuthenticated } = useAuth();
    
    if (!isAuthenticated) {
      return <Login />;
    }
    
    return <Component {...props} />;
  };
}

const ProtectedPage = withAuth(Dashboard);

Render Props Pattern

function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch(url)
      .then(r => r.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, [url]);
  
  return render({ data, loading });
}

// Usage
<DataFetcher 
  url="/api/users"
  render={({ data, loading }) => (
    loading ? <div>Loading...</div> : <div>{data}</div>
  )}
/>

State Management

Local State

import { useState } from 'react';

function ShoppingCart() {
  const [items, setItems] = useState([]);
  
  const addItem = (item) => {
    setItems(prev => [...prev, item]);
  };
  
  const removeItem = (id) => {
    setItems(prev => prev.filter(item => item.id !== id));
  };
  
  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          {item.name}
          <button onClick={() => removeItem(item.id)}>Remove</button>
        </div>
      ))}
    </div>
  );
}

Global State (Without External Libraries)

import { createContext, useContext, useState } from 'react';

const CartContext = createContext();

export function CartProvider({ children }) {
  const [cart, setCart] = useState([]);
  
  const addToCart = (item) => {
    setCart(prev => [...prev, item]);
  };
  
  return (
    <CartContext.Provider value={{ cart, addToCart }}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  return useContext(CartContext);
}

Styling

Component Styles

import styles from './Button.module.css';

function Button({ children }) {
  return (
    <button className={styles.button}>
      {children}
    </button>
  );
}

Dynamic Styles

function Box({ color, size }) {
  return (
    <div style={{
      backgroundColor: color,
      width: `${size}px`,
      height: `${size}px`
    }}>
      Box
    </div>
  );
}

Migration Checklist

  • Understand Svelte’s reactivity model (no Virtual DOM)
  • Convert React components to Svelte components
  • Replace useState with $state
  • Replace useEffect with $effect
  • Replace useMemo with $derived
  • Remove useCallback (not needed)
  • Convert useRef to bind:this
  • Update event handlers (onClickonclick)
  • Replace Context API with Svelte context or stores
  • Convert JSX to Svelte templates
  • Update conditional rendering ({condition && <div>}{#if condition})
  • Update list rendering (.map(){#each})
  • Replace CSS-in-JS/CSS Modules with scoped styles
  • Update form handling (use bind:value)
  • Test all functionality thoroughly

Key Differences to Remember

  1. No Virtual DOM: Svelte compiles to vanilla JavaScript
  2. Built-in Reactivity: No need for setState or hooks
  3. Scoped Styles: CSS is scoped by default
  4. Less Boilerplate: Write less code for the same functionality
  5. True Reactivity: Assignments trigger updates automatically
  6. No Re-renders: Only changed parts of the DOM update

Next Steps