Preact Guide — Lightweight React Alternative for Fast Web Apps
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
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 devThe 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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
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
Assuming full React API parity: Preact doesn’t implement all React APIs (e.g.,
SuspenseandConcurrent Modeare missing). Check compatibility before porting complex React projects.Forgetting
.valuewith signals: Unlike React state (accessed directly), signals require.valueto read/write. Forgetting it returns the signal object, not its value.Using preact/compat without configuring aliases: Simply installing
preact/compatisn’t enough. Your bundler (Vite, Webpack) must aliasreactandreact-domtopreact/compat.Overusing signals for everything: Signals excel for performance-critical, frequently-updating state. For infrequent updates (form submissions, navigation), useState is simpler and sufficient.
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.
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
- What is the main performance advantage of signals over setState?
- How do you use a React library like react-hook-form with Preact?
- What is the difference between
useStateand a signal? - Which React features does Preact NOT implement?
- Why does bundle size matter for web performance?
Answers:
- Signals update only the bound DOM nodes instead of re-rendering entire components and their children. No virtual DOM diffing is involved.
- Use preact/compat by aliasing
reactandreact-domtopreact/compatin your bundler configuration. useStatetriggers a full component re-render when updated. A signal updates only the DOM nodes that reference it, without component re-rendering.- Preact does not implement Concurrent Mode, Suspense (fully), or
React.Childrenutilities. - 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
Try It Yourself
# Create a Preact project
npm create vite@latest my-preact-app -- --template preact
cd my-preact-app
npm install
npm run devReplace 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