Documentation Index Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
Preact is a fast 3kB alternative to React with the same modern API. It delivers the same component-based architecture and hooks as React, but with a significantly smaller footprint and better performance. When you want Reactβs developer experience without the bundle size overhead, Preact is the perfect choice.
Why Preact?
Preact brings all the power of modern React development with exceptional performance:
Tiny Bundle Size - Only 3kB gzipped vs Reactβs ~45kB. Perfect for mobile-first applications.
Blazing Fast - Smaller runtime means faster parsing, faster execution, and better performance on low-end devices.
React Compatible - Use preact/compat to run most React libraries unchanged. Same hooks API, same patterns.
Modern Features - Hooks, fragments, context, concurrent rendering, and more.
Signals - Unique fine-grained reactivity system thatβs even faster than hooks.
No Build Required - Can run directly in browsers with ES modules (though bundling is recommended).
Preact vs React Comparison
Feature Preact React Bundle Size 3kB ~45kB Runtime Speed β‘ Faster β‘ Fast API Compatibility ~95% with compat 100% Hooks β
Yes β
Yes Fragments β
Yes β
Yes Context β
Yes β
Yes Signals β
Unique feature β No DevTools β
Yes β
Yes Ecosystem β οΈ Smaller β
Huge Learning Curve β
Easy (if you know React) β οΈ Moderate TypeScript β
Full support β
Full support Best for Performance-critical apps Large ecosystems
Bundle Size Comparison
Real-world bundle sizes (production, gzipped):
Hello World App:
βββ Preact: ~4 KB (library + app code)
βββ React: ~42 KB (library + app code)
βββ Savings: ~38 KB (90% smaller!)
Todo App:
βββ Preact: ~8 KB
βββ React: ~46 KB
βββ Savings: ~38 KB (82% smaller!)
Full App (with routing):
βββ Preact: ~15 KB
βββ React: ~52 KB
βββ Savings: ~37 KB (71% smaller!)
This means:
Faster initial load (especially on slow connections)
Better performance on mobile devices
Lower bandwidth costs
Improved Core Web Vitals scores
Quick Start
Create a new Preact project with the CLI:
mizu new ./my-preact-app --template frontend/preact
cd my-preact-app
make dev
Visit http://localhost:3000 to see your app!
Project Structure
my-preact-app/
βββ cmd/
β βββ server/
β βββ main.go # Go entry point
βββ app/
β βββ server/
β βββ app.go # Mizu app configuration
β βββ config.go # Server configuration
β βββ routes.go # API routes (Go)
βββ frontend/ # Preact application
β βββ src/
β β βββ main.jsx # App entry point
β β βββ app.jsx # Root component
β β βββ components/
β β β βββ Header.jsx
β β β βββ Footer.jsx
β β β βββ UserCard.jsx
β β βββ pages/
β β β βββ Home.jsx
β β β βββ About.jsx
β β β βββ Users.jsx
β β βββ hooks/
β β β βββ useUsers.js
β β β βββ useAuth.js
β β βββ store/ # Signals store
β β β βββ users.js
β β βββ styles/
β β βββ index.css
β βββ public/
β β βββ favicon.ico
β βββ index.html
β βββ package.json
β βββ vite.config.js
β βββ tsconfig.json
βββ dist/ # Built files
βββ Makefile
Configuration
Vite Configuration
Configure Vite for Preact with React compatibility:
frontend/vite.config.js
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
export default defineConfig ({
plugins: [ preact ()] ,
// React compatibility (optional)
resolve: {
alias: {
'react' : 'preact/compat' ,
'react-dom' : 'preact/compat' ,
'react/jsx-runtime' : 'preact/jsx-runtime'
}
} ,
server: {
port: 5173 ,
strictPort: true ,
hmr: {
clientPort: 3000 // Mizu's port for HMR
}
} ,
build: {
outDir: '../dist' ,
emptyOutDir: true ,
// Optimize for size
rollupOptions: {
output: {
manualChunks: undefined // Keep bundle small
}
},
// Smaller chunk size limit
chunkSizeWarningLimit: 100
}
})
Why use aliases?
Use existing React libraries without modification
Gradual migration from React to Preact
Access to React ecosystem (react-router, etc.)
Backend Configuration
app/server/app.go
package server
import (
" embed "
" io/fs "
" github.com/go-mizu/mizu "
" github.com/go-mizu/mizu/frontend "
)
//go:embed all:../../dist
var distFS embed . FS
func New ( cfg * Config ) * mizu . App {
app := mizu . New ()
// API routes
setupRoutes ( app )
// Frontend middleware
dist , _ := fs . Sub ( distFS , "dist" )
app . Use ( frontend . WithOptions ( frontend . Options {
Mode : frontend . ModeAuto ,
FS : dist ,
Root : "./dist" ,
DevServer : "http://localhost:" + cfg . DevPort ,
IgnorePaths : [] string { "/api" },
}))
return app
}
Preact vs React: Key Differences
While Preact is largely compatible with React, there are some differences:
1. Import Paths
// React
import React from 'react'
import { useState , useEffect } from 'react'
import ReactDOM from 'react-dom'
// Preact (native)
import { h , render } from 'preact'
import { useState , useEffect } from 'preact/hooks'
// Preact (with compat - React-style imports)
import React , { useState , useEffect } from 'react'
import ReactDOM from 'react-dom'
2. JSX Pragma
Preact uses h function instead of React.createElement:
// React (implicit)
< div > Hello </ div >
// Compiles to: React.createElement('div', null, 'Hello')
// Preact (implicit with preset)
< div > Hello </ div >
// Compiles to: h('div', null, 'Hello')
// Preact (explicit pragma - not needed with preset)
/** @jsx h */
import { h } from 'preact'
With @preact/preset-vite, you donβt need to import h manually.
3. Event Naming
Preact uses standard DOM event names:
// React (camelCase)
< button onClick = { handleClick } > Click </ button >
< input onChange = { handleChange } />
// Preact (lowercase also works, but camelCase preferred)
< button onClick = { handleClick } > Click </ button >
< input onInput = { handleInput } /> // Note: onInput instead of onChange
Important: Preact uses onInput for real-time updates, while React uses onChange.
4. Class Names
Both support className, but Preact also supports class:
// React (only className)
< div className = "container" > Content </ div >
// Preact (both work)
< div className = "container" > Content </ div >
< div class = "container" > Content </ div >
5. defaultValue vs value
// React (controlled by default)
< input value = { value } onChange = { onChange } />
// Preact (more flexible)
< input value = { value } onInput = { onInput } />
< input defaultValue = { value } /> // Uncontrolled
Components
Function Components
// Basic component
function Welcome ({ name }) {
return < h1 > Hello, { name } ! </ h1 >
}
// With destructuring and default props
function Card ({ title , content = 'No content' }) {
return (
< div class = "card" >
< h2 > { title } </ h2 >
< p > { content } </ p >
</ div >
)
}
// With TypeScript
interface UserProps {
user : {
id : number
name : string
email : string
}
}
function UserProfile ({ user } : UserProps ) {
return (
< div >
< h1 > { user . name } </ h1 >
< p > { user . email } </ p >
</ div >
)
}
Class Components
While hooks are preferred, class components work too:
import { Component } from 'preact'
class Counter extends Component {
state = { count: 0 }
increment = () => {
this . setState ({ count: this . state . count + 1 })
}
render () {
return (
< div >
< p > Count: { this . state . count } </ p >
< button onClick = { this . increment } > + </ button >
</ div >
)
}
}
Preact supports all React hooks:
useState
import { useState } from 'preact/hooks'
function Counter () {
const [ count , setCount ] = useState ( 0 )
const [ name , setName ] = useState ( 'Guest' )
return (
< div >
< p > { name } : { count } </ p >
< button onClick = { () => setCount ( count + 1 ) } > + </ button >
< input
value = { name }
onInput = { ( e ) => setName ( e . target . value ) }
/>
</ div >
)
}
useEffect
import { useEffect , useState } from 'preact/hooks'
function Users () {
const [ users , setUsers ] = useState ([])
const [ loading , setLoading ] = useState ( true )
useEffect (() => {
fetch ( '/api/users' )
. then ( res => res . json ())
. then ( data => {
setUsers ( data )
setLoading ( false )
})
}, []) // Empty deps = run once on mount
if ( loading ) return < div > Loading... </ div >
return (
< ul >
{ users . map ( user => (
< li key = { user . id } > { user . name } </ li >
)) }
</ ul >
)
}
useReducer
import { useReducer } from 'preact/hooks'
const initialState = { count: 0 }
function reducer ( state , action ) {
switch ( action . type ) {
case 'increment' :
return { count: state . count + 1 }
case 'decrement' :
return { count: state . count - 1 }
case 'reset' :
return initialState
default :
return state
}
}
function Counter () {
const [ state , dispatch ] = useReducer ( reducer , initialState )
return (
< div >
< p > Count: { state . count } </ p >
< button onClick = { () => dispatch ({ type: 'increment' }) } > + </ button >
< button onClick = { () => dispatch ({ type: 'decrement' }) } > - </ button >
< button onClick = { () => dispatch ({ type: 'reset' }) } > Reset </ button >
</ div >
)
}
useContext
import { createContext } from 'preact'
import { useContext } from 'preact/hooks'
const ThemeContext = createContext ( 'light' )
function App () {
return (
< ThemeContext.Provider value = "dark" >
< ThemedButton />
</ ThemeContext.Provider >
)
}
function ThemedButton () {
const theme = useContext ( ThemeContext )
return (
< button class = { `btn- ${ theme } ` } >
Themed Button
</ button >
)
}
useMemo and useCallback
import { useMemo , useCallback , useState } from 'preact/hooks'
function ExpensiveComponent ({ items }) {
const [ filter , setFilter ] = useState ( '' )
// Memoize expensive calculation
const filteredItems = useMemo (() => {
console . log ( 'Filtering items...' )
return items . filter ( item =>
item . name . toLowerCase (). includes ( filter . toLowerCase ())
)
}, [ items , filter ])
// Memoize callback
const handleFilter = useCallback (( e ) => {
setFilter ( e . target . value )
}, [])
return (
< div >
< input value = { filter } onInput = { handleFilter } />
< ul >
{ filteredItems . map ( item => (
< li key = { item . id } > { item . name } </ li >
)) }
</ ul >
</ div >
)
}
import { useRef , useEffect } from 'preact/hooks'
function AutoFocusInput () {
const inputRef = useRef ( null )
useEffect (() => {
// Focus input on mount
inputRef . current ?. focus ()
}, [])
return < input ref = { inputRef } placeholder = "Auto-focused" />
}
Custom Hooks
// hooks/useUsers.js
import { useState , useEffect } from 'preact/hooks'
export function useUsers () {
const [ users , setUsers ] = useState ([])
const [ loading , setLoading ] = useState ( true )
const [ error , setError ] = useState ( null )
useEffect (() => {
fetch ( '/api/users' )
. then ( res => {
if ( ! res . ok ) throw new Error ( 'Failed to fetch' )
return res . json ()
})
. then ( setUsers )
. catch ( setError )
. finally (() => setLoading ( false ))
}, [])
const addUser = async ( user ) => {
const res = await fetch ( '/api/users' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( user )
})
const newUser = await res . json ()
setUsers ([ ... users , newUser ])
}
const deleteUser = async ( id ) => {
await fetch ( `/api/users/ ${ id } ` , { method: 'DELETE' })
setUsers ( users . filter ( u => u . id !== id ))
}
return { users , loading , error , addUser , deleteUser }
}
Usage:
import { useUsers } from './hooks/useUsers'
function UserList () {
const { users , loading , error , deleteUser } = useUsers ()
if ( loading ) return < div > Loading... </ div >
if ( error ) return < div > Error: { error . message } </ div >
return (
< ul >
{ users . map ( user => (
< li key = { user . id } >
{ user . name }
< button onClick = { () => deleteUser ( user . id ) } > Delete </ button >
</ li >
)) }
</ ul >
)
}
Preact Signals
Signals are Preactβs unique fine-grained reactivity system. Theyβre faster and simpler than hooks for state management.
What are Signals?
Signals are reactive primitives that automatically update components when their value changes:
import { signal } from '@preact/signals'
// Create a signal
const count = signal ( 0 )
// Read value
console . log ( count . value ) // 0
// Update value
count . value ++ // Components using count will auto-update!
Why Signals?
Performance:
No re-renders! Components only update the specific DOM nodes that changed
Skip Virtual DOM diffing
Faster than useState for frequently updating state
Simplicity:
No dependency arrays
No useMemo/useCallback needed
Share state without Context API
Size:
Signals add only ~1.6kB to your bundle
Basic Signals
import { signal } from '@preact/signals'
// Global signal (outside component)
const count = signal ( 0 )
function Counter () {
// Signal value is accessed with .value
// But in JSX, you can use the signal directly!
return (
< div >
< p > Count: { count } </ p >
< button onClick = { () => count . value ++ } > + </ button >
< button onClick = { () => count . value -- } > - </ button >
</ div >
)
}
Key points:
Access/update with .value in JavaScript
Use signal directly in JSX (no .value needed)
Changes trigger automatic, fine-grained updates
Computed Signals
Derived values that automatically update:
import { signal , computed } from '@preact/signals'
const count = signal ( 0 )
const doubled = computed (() => count . value * 2 )
function App () {
return (
< div >
< p > Count: { count } </ p >
< p > Doubled: { doubled } </ p >
< button onClick = { () => count . value ++ } > Increment </ button >
</ div >
)
}
Computed signals:
Only recalculate when dependencies change
Cached automatically
Can depend on other computed signals
Effect Signal
Run side effects when signals change:
import { signal , effect } from '@preact/signals'
const count = signal ( 0 )
effect (() => {
console . log ( 'Count changed to:' , count . value )
})
// Now whenever count changes, the effect runs
count . value = 5 // Logs: "Count changed to: 5"
Signals for State Management
Create a global store with signals:
// store/users.js
import { signal , computed } from '@preact/signals'
export const users = signal ([])
export const filter = signal ( '' )
export const filteredUsers = computed (() => {
const f = filter . value . toLowerCase ()
return users . value . filter ( u =>
u . name . toLowerCase (). includes ( f )
)
})
export const userCount = computed (() => users . value . length )
export async function fetchUsers () {
const res = await fetch ( '/api/users' )
users . value = await res . json ()
}
export function addUser ( user ) {
users . value = [ ... users . value , user ]
}
export function deleteUser ( id ) {
users . value = users . value . filter ( u => u . id !== id )
}
Use in components:
import { users , filteredUsers , filter , fetchUsers , deleteUser } from './store/users'
import { useEffect } from 'preact/hooks'
function UserList () {
useEffect (() => {
fetchUsers ()
}, [])
return (
< div >
< input
value = { filter }
onInput = { ( e ) => filter . value = e . target . value }
placeholder = "Filter users..."
/>
< p > Total: { users . value . length } </ p >
< ul >
{ filteredUsers . value . map ( user => (
< li key = { user . id } >
{ user . name }
< button onClick = { () => deleteUser ( user . id ) } > Γ </ button >
</ li >
)) }
</ ul >
</ div >
)
}
Signals vs Hooks
Feature Signals Hooks (useState) Bundle Size +1.6kB Included Performance β‘ Fastest β‘ Fast Re-renders β No β
Yes Global State β
Easy β οΈ Needs Context Computed Values β
Built-in β οΈ useMemo Learning Curve β
Simple β
Familiar Best for Shared state, counters, filters Local component state
Rule of thumb:
Use Signals for: Global state, frequently updated values, shared state
Use Hooks for: Local component state, one-time effects, familiar patterns
Routing
Preact doesnβt include routing, but you have options:
Option 1: preact-router (Recommended)
Lightweight routing made for Preact:
npm install preact-router
import { Router , Route } from 'preact-router'
import Home from './pages/Home'
import About from './pages/About'
import Users from './pages/Users'
import User from './pages/User'
function App () {
return (
< div >
< nav >
< a href = "/" > Home </ a >
< a href = "/about" > About </ a >
< a href = "/users" > Users </ a >
</ nav >
< Router >
< Route path = "/" component = { Home } />
< Route path = "/about" component = { About } />
< Route path = "/users" component = { Users } />
< Route path = "/users/:id" component = { User } />
</ Router >
</ div >
)
}
Access route params:
function User ({ id }) {
return < h1 > User { id } </ h1 >
}
Programmatic navigation:
import { route } from 'preact-router'
function handleClick () {
route ( '/users' )
}
Option 2: React Router (with compat)
Use React Router with preact/compat:
npm install react-router-dom
import { BrowserRouter , Routes , Route , Link } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
function App () {
return (
< BrowserRouter >
< nav >
< Link to = "/" > Home </ Link >
< Link to = "/about" > About </ Link >
</ nav >
< Routes >
< Route path = "/" element = { < Home /> } />
< Route path = "/about" element = { < About /> } />
</ Routes >
</ BrowserRouter >
)
}
Option 3: wouter (Minimalist)
Tiny routing library:
npm install wouter wouter-preact
import { Route , Link } from 'wouter-preact'
function App () {
return (
< div >
< Link href = "/" > Home </ Link >
< Link href = "/about" > About </ Link >
< Route path = "/" component = { Home } />
< Route path = "/about" component = { About } />
</ div >
)
}
React Compatibility (preact/compat)
Run most React libraries unchanged with preact/compat.
Already configured in the Vite config above. Just install React libraries:
npm install react-router-dom
npm install react-query
npm install react-hook-form
Import as normal:
import { useForm } from 'react-hook-form'
import { useQuery } from 'react-query'
function MyForm () {
const { register , handleSubmit } = useForm ()
const { data } = useQuery ( 'users' , () =>
fetch ( '/api/users' ). then ( r => r . json ())
)
return < form onSubmit = { handleSubmit } > ... </ form >
}
What Works
Most React libraries work out of the box:
β
Routing: react-router-dom, wouter
β
Forms: react-hook-form, formik
β
State: zustand, jotai
β
Data Fetching: react-query, swr
β
UI Libraries: Many work (test first)
β
Styling: styled-components, emotion
What Doesnβt Work
β React-specific internals: Libraries using React internals
β React Native: Web only
β Some UI libraries: Material-UI, Chakra (use Preact alternatives)
Testing Compatibility
To test if a library works:
import { useState } from 'react' // Uses preact/compat
import SomeLibrary from 'some-library'
function Test () {
return < SomeLibrary />
}
If it renders without errors, it works!
Styling
CSS Modules
import styles from './Button.module.css'
function Button ({ children }) {
return (
< button class = { styles . button } >
{ children }
</ button >
)
}
/* Button.module.css */
.button {
background : #007bff ;
color : white ;
padding : 0.5 rem 1 rem ;
border : none ;
border-radius : 4 px ;
}
Tailwind CSS
Install Tailwind:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Configure tailwind.config.js:
export default {
content: [
"./index.html" ,
"./src/**/*.{js,jsx,ts,tsx}" ,
] ,
theme: {
extend: {},
} ,
plugins: [] ,
}
Use in components:
function Button () {
return (
< button class = "bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" >
Click me
</ button >
)
}
Styled Components
npm install styled-components
import styled from 'styled-components'
const Button = styled . button `
background: #007bff;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
&:hover {
background: #0056b3;
}
`
function App () {
return < Button > Click me </ Button >
}
Development Workflow
Starting Development
# Option 1: Makefile (recommended)
make dev
# Option 2: Manual (two terminals)
# Terminal 1: Preact dev server
cd frontend
npm run dev
# Terminal 2: Mizu server
go run cmd/server/main.go
Visit http://localhost:3000
Hot Module Replacement
Preact supports Fast Refresh:
Edit components β instant updates
State preserved during updates
No full page reload
Install Preact DevTools extension:
Features:
Inspect component tree
View props and state
Track re-renders
Performance profiling
Building for Production
This:
Builds Preact app (npm run build)
Builds Go binary with embedded frontend
Build Optimizations
The Preact template includes optimizations:
Code splitting:
// Lazy load routes
import { lazy } from 'preact/compat'
const About = lazy (() => import ( './pages/About' ))
< Route path = "/about" component = { About } />
Tree shaking:
// Import only what you need
import { signal } from '@preact/signals' // β
import * as signals from '@preact/signals' // β
Bundle analysis:
npm run build -- --mode analyze
Production Checklist
TypeScript
Preact has excellent TypeScript support:
// Component with props
interface ButtonProps {
onClick : () => void
children : preact . ComponentChildren
variant ?: 'primary' | 'secondary'
}
function Button ({ onClick , children , variant = 'primary' } : ButtonProps ) {
return (
< button class = { `btn- ${ variant } ` } onClick = { onClick } >
{ children }
</ button >
)
}
// With generic types
interface ListProps < T > {
items : T []
renderItem : ( item : T ) => preact . VNode
}
function List < T >({ items , renderItem } : ListProps < T >) {
return (
< ul >
{ items . map (( item , i ) => (
< li key = { i } > { renderItem ( item ) } </ li >
)) }
</ ul >
)
}
Typed Hooks
import { useState } from 'preact/hooks'
// Type inference
const [ count , setCount ] = useState ( 0 ) // number
// Explicit types
const [ user , setUser ] = useState < User | null >( null )
// Typed ref
const inputRef = useRef < HTMLInputElement >( null )
Typed Signals
import { signal , Signal } from '@preact/signals'
// Type inference
const count = signal ( 0 ) // Signal<number>
// Explicit type
const user : Signal < User | null > = signal ( null )
Real-World Example: Todo App with Signals
Complete todo app using Signals:
Backend
// app/server/routes.go
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
var todos = [] Todo {
{ ID : 1 , Title : "Learn Preact" , Completed : true },
{ ID : 2 , Title : "Build app" , Completed : false },
}
func setupRoutes ( app * mizu . App ) {
app . Get ( "/api/todos" , handleTodos )
app . Post ( "/api/todos" , createTodo )
app . Put ( "/api/todos/{id}" , updateTodo )
app . Delete ( "/api/todos/{id}" , deleteTodo )
}
func handleTodos ( c * mizu . Ctx ) error {
return c . JSON ( 200 , todos )
}
Store with Signals
// store/todos.js
import { signal , computed } from '@preact/signals'
export const todos = signal ([])
export const filter = signal ( 'all' ) // 'all' | 'active' | 'completed'
export const newTodoText = signal ( '' )
export const filteredTodos = computed (() => {
switch ( filter . value ) {
case 'active' :
return todos . value . filter ( t => ! t . completed )
case 'completed' :
return todos . value . filter ( t => t . completed )
default :
return todos . value
}
})
export const activeCount = computed (() =>
todos . value . filter ( t => ! t . completed ). length
)
export async function fetchTodos () {
const res = await fetch ( '/api/todos' )
todos . value = await res . json ()
}
export async function addTodo () {
if ( ! newTodoText . value . trim ()) return
const res = await fetch ( '/api/todos' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ title: newTodoText . value , completed: false })
})
const todo = await res . json ()
todos . value = [ ... todos . value , todo ]
newTodoText . value = ''
}
export async function toggleTodo ( id ) {
const todo = todos . value . find ( t => t . id === id )
if ( ! todo ) return
await fetch ( `/api/todos/ ${ id } ` , {
method: 'PUT' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ ... todo , completed: ! todo . completed })
})
todos . value = todos . value . map ( t =>
t . id === id ? { ... t , completed: ! t . completed } : t
)
}
export async function deleteTodo ( id ) {
await fetch ( `/api/todos/ ${ id } ` , { method: 'DELETE' })
todos . value = todos . value . filter ( t => t . id !== id )
}
Components
// App.jsx
import { useEffect } from 'preact/hooks'
import { fetchTodos } from './store/todos'
import TodoForm from './components/TodoForm'
import TodoFilters from './components/TodoFilters'
import TodoList from './components/TodoList'
import './App.css'
export function App () {
useEffect (() => {
fetchTodos ()
}, [])
return (
< div class = "app" >
< h1 > Todos </ h1 >
< TodoForm />
< TodoFilters />
< TodoList />
</ div >
)
}
// components/TodoForm.jsx
import { newTodoText , addTodo } from '../store/todos'
export default function TodoForm () {
const handleSubmit = ( e ) => {
e . preventDefault ()
addTodo ()
}
return (
< form onSubmit = { handleSubmit } class = "todo-form" >
< input
value = { newTodoText }
onInput = { ( e ) => newTodoText . value = e . target . value }
placeholder = "What needs to be done?"
class = "todo-input"
/>
< button type = "submit" > Add </ button >
</ form >
)
}
// components/TodoFilters.jsx
import { filter , activeCount } from '../store/todos'
export default function TodoFilters () {
return (
< div class = "filters" >
< button
class = { filter . value === 'all' ? 'active' : '' }
onClick = { () => filter . value = 'all' }
>
All
</ button >
< button
class = { filter . value === 'active' ? 'active' : '' }
onClick = { () => filter . value = 'active' }
>
Active ( { activeCount } )
</ button >
< button
class = { filter . value === 'completed' ? 'active' : '' }
onClick = { () => filter . value = 'completed' }
>
Completed
</ button >
</ div >
)
}
// components/TodoList.jsx
import { filteredTodos , toggleTodo , deleteTodo } from '../store/todos'
export default function TodoList () {
if ( filteredTodos . value . length === 0 ) {
return < p class = "empty" > No todos found </ p >
}
return (
< ul class = "todo-list" >
{ filteredTodos . value . map ( todo => (
< li key = { todo . id } class = { todo . completed ? 'completed' : '' } >
< input
type = "checkbox"
checked = { todo . completed }
onChange = { () => toggleTodo ( todo . id ) }
/>
< span > { todo . title } </ span >
< button onClick = { () => deleteTodo ( todo . id ) } > Γ </ button >
</ li >
)) }
</ ul >
)
}
Migration from React
Step 1: Install Preact
npm install preact
npm install -D @preact/preset-vite
Step 2: Update Vite Config
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/jsx-runtime' : 'preact/jsx-runtime'
}
}
})
Step 3: Update Imports (Gradual)
// Before (React)
import React , { useState } from 'react'
import ReactDOM from 'react-dom'
// After (keep same - compat aliased)
import React , { useState } from 'react'
import ReactDOM from 'react-dom'
// Or switch to Preact native (optional, over time)
import { useState } from 'preact/hooks'
import { render } from 'preact'
Step 4: Test
Run your app. Most things should work immediately!
Step 5: Optimize (Optional)
Replace compat imports with Preact native:
// From
import { useState } from 'react'
// To
import { useState } from 'preact/hooks'
Replace React Router with preact-router:
// From
import { BrowserRouter , Route } from 'react-router-dom'
// To
import { Router , Route } from 'preact-router'
Consider using Signals for state management.
1. Use Signals for Shared State
// β Slower: Context + useState
const [ users , setUsers ] = useState ([])
// β
Faster: Signals
const users = signal ([])
2. Lazy Load Routes
import { lazy } from 'preact/compat'
const About = lazy (() => import ( './pages/About' ))
3. Avoid Inline Functions
// β Creates new function on every render
< button onClick = { () => handleClick ( id ) } > Click </ button >
// β
Reuse function
const onClick = () => handleClick ( id )
< button onClick = { onClick } > Click </ button >
4. Use Keys in Lists
// β
Proper keys
{ items . map ( item => < li key = { item . id } > { item . name } </ li > )}
// β Index as key (avoid if list changes)
{ items . map (( item , i ) => < li key = { i } > { item . name } </ li > )}
5. Measure Bundle Size
npm run build
ls -lh dist/assets/ * .js
Aim for:
Total JS: <20kB (with routing)
Main bundle: <15kB
Troubleshooting
h is not defined
Error:
ReferenceError: h is not defined
Cause: Not using @preact/preset-vite
Solution: Install preset:
npm install -D @preact/preset-vite
Update vite.config.js:
import preact from '@preact/preset-vite'
export default {
plugins: [ preact ()]
}
React Library Doesnβt Work
Symptom: React library throws errors
Solution: Check if compat is configured:
// vite.config.js
export default {
resolve: {
alias: {
'react' : 'preact/compat' ,
'react-dom' : 'preact/compat'
}
}
}
If still broken, the library may use React internals. Find a Preact alternative.
onChange Not Firing
Cause: Preact uses onInput for real-time updates
Solution:
// React
< input onChange = { handleChange } />
// Preact
< input onInput = { handleChange } />
Solution: Install Preact DevTools (not React DevTools):
Bundle Size Too Large
Check:
npm run build
ls -lh dist/assets/
Solutions:
Remove unused dependencies
Use dynamic imports for routes
Check for duplicate React/Preact
Use Signals instead of heavy state libraries
When to Choose Preact
Choose Preact When:
β
Bundle size is critical (mobile, emerging markets)
β
You want React DX with better performance
β
Building performance-critical applications
β
You need fine-grained reactivity (Signals)
β
Migrating from React but want smaller bundle
β
Every kilobyte counts
β
You value simplicity and speed
Choose React When:
β
You need the full React ecosystem
β
Using React-specific libraries
β
Team is deeply invested in React
β
Bundle size doesnβt matter
β
Need bleeding-edge React features first
Choose Something Else When:
Need SSR/SSG: Use Next.js, Nuxt, or SvelteKit
Want full framework: Use Nuxt, Next.js, or Angular
Hate JSX: Use Vue or Svelte
Want even smaller: Use vanilla JS or Alpine.js
Next Steps
React Guide Compare with full React setup
Signals Documentation Deep dive into Signals
Preact Router Routing documentation
preact/compat React compatibility guide