Skip to content
Preact Guide — Lightweight React Alternative for Fast Web Apps

Preact Guide — Lightweight React Alternative for Fast Web Apps

DodaTech Updated Jun 7, 2026 8 min read

Preact is a fast 3kB alternative to React with the same modern API, supporting hooks, fragments, and the Context API while adding unique features like signals for even better performance in a fraction of the bundle size.

What You’ll Learn

You’ll understand how Preact differs from React, use the same hooks API in a smaller bundle, manage state with signals, integrate React libraries via preact/compat, and build single-page apps with wouter routing.

Why Preact Matters

Bundle size matters for web performance. React with react-dom weighs about 40kB minified. Preact delivers nearly the same developer experience in 3kB — a 93% reduction. This translates to faster page loads, better Core Web Vitals, and improved conversion rates. Doda Browser’s lightweight extension UI uses Preact because every kilobyte matters when shipping browser add-ons.

Preact Learning Path

    flowchart LR
  A[React Concepts] --> B[Preact]
  B --> C[Hooks API]
  B --> D[Signals]
  B --> E[preact/compat]
  B --> F[Routing with wouter]
  C --> G[useState, useEffect]
  D --> H[Fine-Grained Reactivity]
  B:::current

  classDef current fill:#673AB8,color:#fff,stroke:#333,stroke-width:2px
  
Prerequisites: Familiarity with React concepts (components, hooks, JSX). Solid JavaScript fundamentals are expected.

Preact vs React — What’s Different

Preact aims for API compatibility with React but takes a fundamentally different approach under the hood:

  • No synthetic events — Preact uses native browser events directly, reducing overhead
  • Smaller compatibility layer — Only implements the most-used React features
  • Signals — First-class reactive primitives for performance-critical updates
  • No concurrent mode — Keeps the library simple and predictable

The most notable compatibility feature is preact/compat, which wraps Preact to match React’s API exactly. Most React libraries work through it.

Getting Started

# Create a Preact project with Vite
npm create vite@latest my-preact-app -- --template preact
cd my-preact-app
npm install
npm run dev

The Vite template sets up Preact with JSX configuration and hot module replacement.

Hooks in Preact

Preact supports React’s most common hooks out of the box:

import { useState, useEffect, useCallback } from 'preact/hooks';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  const search = useCallback(async (q) => {
    if (q.length < 2) {
      setResults([]);
      return;
    }
    
    setLoading(true);
    // Simulate API call
    const res = await fetch(`https://api.example.com/search?q=${q}`);
    const data = await res.json();
    setResults(data.items);
    setLoading(false);
  }, []);

  useEffect(() => {
    const timer = setTimeout(() => search(query), 300);
    return () => clearTimeout(timer); // Debounce cleanup
  }, [query, search]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onInput={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {loading && <p>Searching...</p>}
      <ul>
        {results.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

Output: Typing in the input debounces for 300ms, then fires a search. Results render in the list below. Loading state shows during the fetch. The component unmounts cleanly via the useEffect cleanup function.

Signals — Preact’s Secret Weapon

Signals provide fine-grained reactivity without re-rendering entire components. Unlike React’s useState (which re-renders the whole component tree), signals update only the specific DOM nodes that depend on them:

import { signal, computed } from '@preact/signals';

// Create a signal
const count = signal(0);
const name = signal('World');

// Computed signals derive values automatically
const greeting = computed(() => `Hello, ${name.value}!`);
const doubled = computed(() => count.value * 2);

// Signals update only the text nodes that reference them
function Counter() {
  return (
    <div>
      <p>{count}</p>           {/* Only this text node updates on click */}
      <p>Doubled: {doubled}</p> {/* This text node updates too */}
      <button onClick={() => count.value++}>
        Increment
      </button>
    </div>
  );
}

Output: Clicking the button increments count. Only the two <p> elements with signal bindings update — the button itself, its parent <div>, and any other sibling elements do not re-render. This is fundamentally more efficient than React’s virtual DOM diffing.

Why Signals Matter for Performance

In React, a state change in a deeply nested component triggers re-rendering of that component and all its children. With signals:

  • Components render once — only the bound DOM nodes update
  • No virtual DOM diffing needed
  • Updates happen in microseconds regardless of component tree depth

This pattern is ideal for real-time features like live search results, notification counters, or progress indicators.

preact/compat — Using React Libraries

The compatibility layer lets you use thousands of React components with Preact:

// vite.config.js
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
  plugins: [
    preact({
      // Enable preact/compat aliasing
      reactAliasesEnabled: true
    })
  ]
});
// With compat aliased, you can import React libraries directly
import { useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import DatePicker from 'react-datepicker';

function BookingForm() {
  const { register, handleSubmit, errors } = useForm();

  const onSubmit = (data) => {
    toast.success('Booking confirmed!');
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name', { required: true })} />
      {errors.name && <p>Name is required</p>}
      
      <DatePicker {...register('date')} />
      
      <button type="submit">Book Now</button>
    </form>
  );
}

Output: React Hook Form and React DatePicker work seamlessly through preact/compat. The bundle stays small (Preact + compat layer ≈ 5kB) while using familiar React ecosystem tools.

Routing with Wouter

Wouter is a tiny router (1.3kB) that pairs perfectly with Preact:

import { Router, Route, Link, Switch } from 'wouter-preact';

function App() {
  return (
    <div>
      <nav>
        <Link href="/">Home</Link>
        <Link href="/about">About</Link>
        <Link href="/users">Users</Link>
      </nav>

      <Switch>
        <Route path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/users/:id" component={UserProfile} />
        <Route component={NotFound} />
      </Switch>
    </div>
  );
}

function UserProfile({ params }) {
  return <h1>User: {params.id}</h1>;
}

Output: Navigating between routes renders the matching component. The <Link> component handles client-side navigation without page reloads. Dynamic segments like :id are passed as route params.

Security Angle: Sanitizing User Input

Preact doesn’t escape HTML automatically in signal text interpolation. Always sanitize user-generated content:

import { signal } from '@preact/signals';

function sanitize(str) {
  const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;' };
  return str.replace(/[&<>"']/g, (m) => map[m]);
}

const userInput = signal('');

function CommentBox() {
  return (
    <div>
      <textarea onInput={(e) => userInput.value = e.target.value} />
      {/* Always sanitize before rendering user input */}
      <div dangerouslySetInnerHTML={{ __html: sanitize(userInput.value) }} />
    </div>
  );
}

DodaTech uses similar sanitization in Doda Browser’s bookmark manager to prevent XSS attacks when sharing bookmarks with HTML descriptions.

Common Mistakes Beginners Make

  1. Assuming full React API parity: Preact doesn’t implement all React APIs (e.g., Suspense and Concurrent Mode are missing). Check compatibility before porting complex React projects.

  2. Forgetting .value with signals: Unlike React state (accessed directly), signals require .value to read/write. Forgetting it returns the signal object, not its value.

  3. Using preact/compat without configuring aliases: Simply installing preact/compat isn’t enough. Your bundler (Vite, Webpack) must alias react and react-dom to preact/compat.

  4. Overusing signals for everything: Signals excel for performance-critical, frequently-updating state. For infrequent updates (form submissions, navigation), useState is simpler and sufficient.

  5. Expecting React DevTools to work: Preact has its own DevTools extension. React DevTools won’t detect Preact components, making debugging harder without the right tools.

  6. Destructuring signal values in JSX: {count.value} captures the value at render time, losing reactivity. Use {count} (the signal itself) in JSX for reactive bindings.

Practice Questions

  1. What is the main performance advantage of signals over setState?
  2. How do you use a React library like react-hook-form with Preact?
  3. What is the difference between useState and a signal?
  4. Which React features does Preact NOT implement?
  5. Why does bundle size matter for web performance?

Answers:

  1. Signals update only the bound DOM nodes instead of re-rendering entire components and their children. No virtual DOM diffing is involved.
  2. Use preact/compat by aliasing react and react-dom to preact/compat in your bundler configuration.
  3. useState triggers a full component re-render when updated. A signal updates only the DOM nodes that reference it, without component re-rendering.
  4. Preact does not implement Concurrent Mode, Suspense (fully), or React.Children utilities.
  5. Smaller bundles mean faster downloads, earlier JavaScript parsing, and better Core Web Vitals (LCP, FID, CLS), directly impacting user experience and SEO rankings.

Challenge

Build a real-time dashboard with Preact signals: create signals for system metrics (CPU, memory, network), simulate periodic updates with setInterval, display each metric with a computed signal for formatted output, add a toggle to pause/resume updates, and use wouter for a settings page.

Real-World Task

Create a browser extension popup using Preact and signals. The popup should display the current page title, URL, and a notes field saved to storage. Use signals for reactive form state and wouter for tab navigation within the popup.

FAQ

Is Preact a drop-in replacement for React?
: For most apps, yes. Through preact/compat, you get near-perfect React API compatibility. Some advanced features (Concurrent Mode, full Suspense) are missing, but the majority of projects won’t notice the difference.
Does Preact have its own DevTools?
: Yes. Install the Preact DevTools browser extension for component inspection, signal monitoring, and performance profiling.
Can I migrate an existing React app to Preact?
: Usually. Install preact and @preact/preset-vite, configure react and react-dom aliases, and test thoroughly. Most React libraries work without changes.
How small is a real Preact app?
: A minimal Preact app with signals and the compat layer starts around 5-7kB minified + gzipped. Compare to 45-50kB for a comparable React setup.
What companies use Preact in production?
: Uber (web version), Comcast, and Doda Browser use Preact for performance-critical UIs where bundle size directly impacts user experience.

Try It Yourself

# Create a Preact project
npm create vite@latest my-preact-app -- --template preact
cd my-preact-app
npm install
npm run dev

Replace the default counter with a signal-based version. Add a second signal for a text input, a computed signal that displays the character count, and a wouter-based routing setup with two pages.

What’s Next

Related topics: React, JavaScript, TypeScript, Vite

What’s Next

Congratulations on completing this Preact tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro