What is Preact?
Preact is a tiny alternative to React. It implements the same component model, the same hooks and a virtual DOM, in around three kilobytes gzipped. For most components, the code you write for React works on Preact with little or no change.
That size is the whole point. When you are embedding a widget in a page you do not fully control, building a performance-critical interface or simply want to keep your JavaScript budget small, a few kilobytes of framework can make a measurable difference. Preact gives you the familiar model without the weight.
Components and hooks
Preact components are functions that return JSX, and hooks come from preact/hooks.
// Search.jsx
import { useState } from "preact/hooks";
export function Search({ onSearch }) {
const [query, setQuery] = useState("");
return (
<form
onSubmit={(event) => {
event.preventDefault();
onSearch(query);
}}
>
<input
value={query}
onInput={(event) => setQuery(event.currentTarget.value)}
/>
</form>
);
}
useState, useEffect, useRef, useContext, useMemo and useCallback all behave as you would expect. The main differences are small: Preact supports both class and className, and uses native event names such as onInput and onDblClick.
Preact Signals
Signals are Preact’s first-class reactivity primitive. A signal holds a value and, when read inside JSX, subscribes that part of the DOM to it. Changing the value updates the DOM directly — no component re-render required.
// Counter.jsx
import { signal, computed } from "@preact/signals";
const count = signal(0);
const double = computed(() => count.value * 2);
export function Counter() {
return (
<div>
<button onClick={() => count.value++}>
Count: {count}
</button>
<p>Double: {double}</p>
</div>
);
}
Because signals live outside the component tree, you can share and mutate them from anywhere, including plain modules and event handlers. They are the recommended way to manage state that changes often.
The compat layer
The preact/compat package maps React’s API onto Preact. Alias react and react-dom to it in your bundler, and most React libraries work unchanged.
// vite.config.js
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";
export default defineConfig({
plugins: [preact()],
resolve: {
alias: {
react: "preact/compat",
"react-dom": "preact/compat",
"react-dom/client": "preact/compat/client",
},
},
});
This is how Preact projects use the React ecosystem — component libraries, routing, state managers and more — while still shipping the smaller runtime. The trade-off is a little overhead from the compatibility layer, but it is still far smaller than React itself.
No build step with htm
If you want to skip the build entirely, the htm library provides a tagged template alternative to JSX.
// app.js
import { h, render } from "preact";
import htm from "htm";
const html = htm.bind(h);
function App({ name }) {
return html`<h1>Hello, ${name}!</h1>`;
}
render(html`<${App} name="Ada" />`, document.body);
This is handy for progressive enhancement, quick demos and embedding a small interactive island into a server-rendered page.
Differences from React
The overlap is large, but a few details differ:
- Preact uses native DOM event names, so
onInputandonDblClickreplace some React-specific names. - Both
classandclassNamework, and bothforandhtmlForare accepted. - There is no synthetic event pooling, because Preact never pooled events.
- Some newer React features, such as parts of the server-components API, are not implemented.
- TypeScript JSX types come from Preact, and need aliasing when using compat.
For everyday components and hooks, you will rarely notice the difference.
When to use Preact
Preact shines when size and startup cost are priorities: embeddable widgets, marketing pages with a little interactivity, content sites, and applications with strict performance budgets. It is also a gentle migration target for an existing React codebase that needs to get smaller.
If you rely on React-specific tooling, bleeding-edge features or a library that depends on React internals, staying on React may be less friction. The good news is that the API overlap means the decision is not a one-way door.
Best practices
- Use signals for state that changes frequently or is shared outside the tree.
- Reach for
preact/compatonly when you need React ecosystem packages. - Alias React types as well as runtime modules when using compat.
- Keep components small and avoid unnecessary re-renders by reading signals at the point of use.
- Measure the real bundle impact — the savings are meaningful, but your own code still dominates.
- Prefer
preact/preset-viteto configure the build correctly out of the box.
Common mistakes
- Mixing React and Preact packages without the compat alias.
- Forgetting to alias
react-dom/client, which breaks the new root API. - Assuming every React library works without testing the compat layer.
- Overusing signals and re-rendering components that could read a signal directly.
- Treating Preact as feature-identical to React and being surprised by an edge case.
- Adding Preact for size, then pulling in a large dependency that erases the gain.
Where to go next
Preact is the pragmatic answer when React’s model is right but its size is not. Compare it with React and Svelte, wire it up with Vite, and keep your JavaScript fundamentals close. Then try embedding a small Preact widget in a plain HTML page to see how little it costs.