Rendering is the process of converting your components into actual DOM elements that appear in the browser.
The render function
Preact’s render() function mounts a component tree into a DOM element (src/render.js:12):
export function render ( vnode , parentDom ) {
// https://github.com/preactjs/preact/issues/3794
if ( parentDom == document ) {
parentDom = document . documentElement ;
}
if ( options . _root ) options . _root ( vnode , parentDom );
let isHydrating = vnode && vnode . _flags & MODE_HYDRATE ;
// To be able to support calling `render()` multiple times on the same
// DOM node, we need to obtain a reference to the previous tree.
let oldVNode = isHydrating ? NULL : parentDom . _children ;
parentDom . _children = createElement ( Fragment , NULL , [ vnode ]);
// List of effects that need to be called after diffing.
let commitQueue = [],
refQueue = [];
diff (
parentDom ,
parentDom . _children ,
oldVNode || EMPTY_OBJ ,
EMPTY_OBJ ,
parentDom . namespaceURI ,
oldVNode
? NULL
: parentDom . firstChild
? slice . call ( parentDom . childNodes )
: NULL ,
commitQueue ,
oldVNode ? oldVNode . _dom : parentDom . firstChild ,
isHydrating ,
refQueue ,
parentDom . ownerDocument
);
// Flush all queued effects
commitRoot ( commitQueue , parentDom . _children , refQueue );
}
Basic usage
Import render and mount your app:
import { render } from 'preact' ;
function App () {
return < h1 > Hello, World! </ h1 > ;
}
render ( < App /> , document . body );
Rendering to document is automatically converted to document.documentElement to avoid issues.
Mounting to a container
It’s common to render into a dedicated container element:
<! DOCTYPE html >
< html >
< body >
< div id = "app" ></ div >
< script src = "bundle.js" ></ script >
</ body >
</ html >
import { render } from 'preact' ;
import App from './App' ;
render ( < App /> , document . getElementById ( 'app' ));
Initial render vs updates
Preact handles initial renders and subsequent updates differently:
Initial render
On first render, Preact:
Creates a VNode tree from your components
Generates real DOM nodes
Inserts them into the parent container
Calls lifecycle methods like componentDidMount
Subsequent renders
When you call render() again on the same container:
Preact finds the previous VNode tree (stored in parentDom._children)
Diffs the new tree against the old one
Only updates the parts that changed
Calls update lifecycle methods
Preact automatically detects if you’re rendering into a container that already has a Preact app and performs an efficient update.
Replacing content
When you render into a container, Preact replaces any existing content:
< div id = "app" >
< p > Loading... </ p >
</ div >
The <p>Loading...</p> is completely replaced by the rendered content.
Hydration
Hydration is the process of attaching Preact to server-rendered HTML instead of replacing it (src/render.js:66):
export function hydrate ( vnode , parentDom ) {
vnode . _flags |= MODE_HYDRATE ;
render ( vnode , parentDom );
}
When to use hydrate
Use hydrate() when:
You’re using server-side rendering (SSR)
The DOM already contains the rendered HTML
You want to attach event listeners and make it interactive
Hydration example
Server-rendered HTML
Client-side hydration
<! DOCTYPE html >
< html >
< body >
< div id = "app" >
< h1 > Hello, World! </ h1 >
< p > This was rendered on the server </ p >
</ div >
< script src = "bundle.js" ></ script >
</ body >
</ html >
import { hydrate } from 'preact' ;
function App () {
return (
< div >
< h1 > Hello, World! </ h1 >
< p > This was rendered on the server </ p >
</ div >
);
}
hydrate ( < App /> , document . getElementById ( 'app' ));
The component tree passed to hydrate() must match the server-rendered HTML exactly, or Preact will log warnings.
Rendering process
Here’s what happens when you call render():
Step-by-step breakdown
Prepare the VNode - The component is wrapped in a Fragment
Find previous tree - Check if this container was rendered before
Diff - Compare new and old VNode trees
Update DOM - Apply minimal changes to real DOM
Commit - Make all DOM changes visible at once
Effects - Run refs, lifecycle callbacks, etc.
Real-world example
Here’s a complete example from Preact’s demo (demo/index.jsx:197):
import { render , Component } from 'preact' ;
import { Router , Link } from 'preact-router' ;
import Home from './home' ;
import Todo from './todo' ;
import Context from './context' ;
class App extends Component {
render ({ url }) {
return (
< div class = "app" >
< header >
< nav >
< Link href = "/" activeClassName = "active" > Home </ Link >
< Link href = "/todo" activeClassName = "active" > ToDo </ Link >
< Link href = "/context" activeClassName = "active" > Context </ Link >
</ nav >
</ header >
< main >
< Router url = { url } >
< Home path = "/" />
< Todo path = "/todo" />
< Context path = "/context" />
</ Router >
</ main >
</ div >
);
}
}
render ( < App /> , document . body );
This example:
Imports render from Preact
Defines an App component
Renders it into document.body
Sets up client-side routing
Multiple render calls
You can call render() multiple times on the same container to update the UI:
import { render } from 'preact' ;
let count = 0 ;
const container = document . getElementById ( 'app' );
function update () {
render ( < h1 > Count: { count } </ h1 > , container );
}
update (); // Initial render
setInterval (() => {
count ++ ;
update (); // Updates existing render
}, 1000 );
Each call to render() on the same container performs an efficient diff and only updates what changed.
Unmounting
To unmount a component tree, render null:
import { render } from 'preact' ;
const container = document . getElementById ( 'app' );
// Mount app
render ( < App /> , container );
// Later: unmount and clean up
render ( null , container );
This:
Calls componentWillUnmount() on all components
Removes all DOM nodes
Cleans up event listeners and refs
Rendering to specific elements
You can render multiple independent Preact apps on the same page:
import { render } from 'preact' ;
// Render header
render (
< Header /> ,
document . getElementById ( 'header' )
);
// Render main app
render (
< App /> ,
document . getElementById ( 'app' )
);
// Render footer
render (
< Footer /> ,
document . getElementById ( 'footer' )
);
Each render is independent and maintains its own state.
Rendering fragments
You can render fragments directly:
import { render , Fragment } from 'preact' ;
render (
< Fragment >
< h1 > Title </ h1 >
< p > Content </ p >
</ Fragment > ,
document . getElementById ( 'app' )
);
This renders both elements as siblings inside the container.
Avoid rendering to document.body
Rendering directly to document.body can interfere with browser extensions and dev tools:
render ( < App /> , document . getElementById ( 'app' ));
Reuse containers
When updating, always render to the same container:
Correct - Same container
Wrong - Different containers
const container = document . getElementById ( 'app' );
render ( < App count = { 1 } /> , container );
render ( < App count = { 2 } /> , container ); // Efficient update
Error boundaries
Wrap your render in a try-catch for error handling:
import { render } from 'preact' ;
import App from './App' ;
import ErrorFallback from './ErrorFallback' ;
try {
render ( < App /> , document . getElementById ( 'app' ));
} catch ( error ) {
console . error ( 'Failed to render app:' , error );
render (
< ErrorFallback error = { error } /> ,
document . getElementById ( 'app' )
);
}
For better error handling, consider using error boundary components that catch errors in their child components.