Rendering API
render
function render(vnode: VNode | null, container: Element): voidRender a VNode tree into a DOM container. On first call, mounts the tree. On subsequent calls, diffs against the previous tree and applies minimal DOM mutations. Pass null to unmount.
import { render, h } from "tachys"
render(h("div", null, "Hello"), document.getElementById("app")!)
// Update
render(h("div", null, "World"), document.getElementById("app")!)
// Unmount
render(null, document.getElementById("app")!)createRoot
interface Root {
render(children: VNode): void
unmount(): void
}
function createRoot(container: Element): RootCreates a concurrent root for the given DOM container and returns a Root object. This is the React 18+ root API. Call root.render() to mount or update the tree, and root.unmount() to tear it down.
import { createRoot, h } from "tachys"
const root = createRoot(document.getElementById("app")!)
root.render(h("div", null, "Hello"))
// Update
root.render(h("div", null, "World"))
// Tear down
root.unmount()hydrateRoot
function hydrateRoot(container: Element, initialChildren: VNode): RootHydrates server-rendered HTML in container using initialChildren as the expected VNode tree, then returns a Root for subsequent updates. Reuses existing DOM nodes where possible instead of replacing them.
import { hydrateRoot, h } from "tachys"
const root = hydrateRoot(document.getElementById("app")!, h(App, null))
root.render(h(App, null))mount
function mount(vnode: VNode, parentDom: Element): voidLower-level mount that attaches a VNode tree to a DOM element. Does not track previous trees for diffing. Use render for most cases.
patch
function patch(oldVNode: VNode, newVNode: VNode, parentDom: Element): voidDiff an existing VNode tree against a new one and apply minimal DOM mutations.
unmount
function unmount(vnode: VNode): voidUnmount a VNode tree, cleaning up event listeners, refs, component instances, and returning VNodes to the pool.
h
function h(
type: string | ComponentFn | null,
props: Record<string, unknown> | null,
...children: Array<VNode | string | number | boolean | null | undefined>
): VNodeCreate a VNode. This is the classic hyperscript / createElement API.
// Element
h("div", { className: "box" }, "Hello")
// Component
h(MyComponent, { name: "World" })
// Fragment
h(null, null, h("li", null, "A"), h("li", null, "B"))
// Nested
h("ul", null, h("li", null, "One"), h("li", null, "Two"))createTextVNode
function createTextVNode(text: string): VNodeCreate a text VNode directly.
Scheduler
Lane
const Lane = { Sync: 0, Default: 1, Transition: 2, Idle: -1 } as constPriority lanes for the scheduler:
| Lane | Value | Description |
|---|---|---|
Sync | 0 | Highest priority. Used by useSyncExternalStore for tearing prevention. |
Default | 1 | Normal state updates from useState, useReducer. |
Transition | 2 | Low priority. Used by startTransition, useTransition, useDeferredValue. |
Idle | -1 | Sentinel for "no lane active". Never scheduled explicitly. |
Two-phase commit
Transition-lane renders run in two phases. The render phase walks the VNode tree and collects DOM mutations into a typed effect queue instead of mutating the DOM directly. The commit phase flushes the queue atomically after the render completes.
The effect queue lets Tachys:
- Abandon an in-progress Transition when a higher-priority update arrives (discarding the queue costs nothing, no DOM rollback needed). Hook state and ref callbacks from the abandoned render are also rolled back.
- Suspend cleanly when a component throws a promise during a Transition. The scheduler retries when the promise resolves instead of committing a Suspense fallback.
- Yield mid-render (keyed and non-keyed children diffing check a ~5ms time slice) and resume on the next tick.
Sync and Default renders skip the effect queue and mutate the DOM directly. The R.collecting flag read is branch-predicted-false on the hot path and folded away by the JIT.
flushUpdates
function flushUpdates(): voidSynchronously flush all pending state updates across all lanes. Normally updates are batched via microtask. Call this in tests or when you need synchronous rendering.
flushSyncWork
function flushSyncWork(): voidFlush only the Sync lane. Useful when you need to ensure useSyncExternalStore updates are processed before other work.
shouldYield
function shouldYield(): booleanReturns true if the current time slice (~5ms) has expired. Used internally by the work loop.
act
async function act(callback: () => void | Promise<void>): Promise<void>Testing utility that wraps a callback triggering state updates and synchronously flushes all pending work, including microtasks and async effects. Compatible with React Testing Library's act() usage.
INFO
act is imported from tachys/compat, not the core tachys package.
import { act } from "tachys/compat"
import { render, h } from "tachys"
await act(async () => {
render(h(MyComponent, null), container)
})
// DOM is fully updated here