API Reference
A tour of the most-used What Framework APIs across 14 modules. For the complete export surface, read the bundled type declarations.
Reactivity
The core primitives for building reactive applications.
| Function | Description |
signal(value) | Create a reactive value. Returns getter function with .set(), .peek(), .subscribe() |
computed(fn) | Create a derived value that auto-updates when dependencies change |
effect(fn) | Run a side effect when dependencies change. Returns dispose function |
batch(fn) | Batch multiple signal writes: effects run once after batch completes. Returns nothing, the callback's value is discarded |
untrack(fn) | Read signals without creating subscriptions |
flushSync() | Force all pending effects to run synchronously |
Rendering
Functions for creating and mounting elements.
| Function | Description |
h(tag, props, ...children) | Create an element (advanced/internal). JSX is compiled by the optimizer into direct DOM operations |
mount(element, container) | Render an element tree into the DOM. Returns unmount function |
Fragment({ children }) | Group children without a wrapper element |
html`...` | Tagged template literal for HTML. No build step required |
Control Flow Components
| Component | Description |
<Show when fallback> | Conditionally render children based on a condition |
<For each fallback> | Render a list, with fallback shown while the list is empty on both the compiled and the runtime path. Keyed reconciliation needs the What compiler: on the runtime path key is ignored and every row is rebuilt on each change |
<Switch> / <Match when> | Render one of multiple conditions |
<Portal target> | Render children into a different DOM container |
<ErrorBoundary fallback onError> | Catch errors in child components |
<Suspense fallback> | Show fallback while lazy components load |
memo(Component, areEqual) | No-op pass-through wrapper, kept for React source compatibility. There is no re-render to skip in the run-once model, and areEqual is never called |
lazy(loader) | Code-split a component. Works with Suspense on the client. Under SSR only renderToStream resolves it; renderToString and renderToStringAsync emit the Suspense fallback |
<Island component mode> | Deferred hydration: load, idle, visible, interaction, media |
Hooks
React-compatible hooks backed by signals.
| Hook | Description |
useState(initial) | Returns [Signal<T>, setter]. The first element is the signal itself, not a snapshot: read it with count() and it also carries .peek() and .set(). In JSX pass {count} bare so the binding stays live. Components run once, so the setter drives fine-grained DOM effects, it does not re-run the component |
useSignal(initial) | Returns the raw signal for direct signal() and signal.set() access |
useComputed(fn) | Returns a computed signal (read-only derived value) |
useEffect(fn, deps) | Side effect after mount. Return a function from fn to register cleanup; useEffect itself returns nothing. No deps auto-tracks signals, [] runs once, [a, b] expects signal accessors |
useMemo(fn, deps) | Memoized computation. Returns a Computed<T> accessor, not the value, so call it to read: const total = useMemo(() => a() + b()); total(). deps is accepted for React familiarity and ignored: the computation auto-tracks the signals it reads |
useCallback(fn, deps) | Memoized callback function |
useRef(initial) | Mutable ref that doesn't trigger re-renders. Returns { current } |
useReducer(reducer, initial, init) | State with a reducer function. Returns [Signal<S>, dispatch]: the first element is the signal itself, so read it with state() and pass it bare into JSX to keep the binding live. The optional third argument is the lazy initializer, applied to initial once |
Lifecycle
| Function | Description |
onMount(fn) | Run code once after component mounts to the DOM |
onCleanup(fn) | Register cleanup function for component unmount |
createResource(fetcher, opts) | Reactive data fetching. Returns [data, { loading, error, refetch, mutate }] |
Context
| Function | Description |
createContext(default) | Create a context with .Provider component |
useContext(Context) | Access the nearest context value |
Data Fetching
SWR, query, and cache management primitives. Learn more →
| Function | Description |
useFetch(url, options) | Simple fetch wrapper. Returns data(), error(), isLoading(), refetch() |
useSWR(key, fetcher, opts) | Stale-while-revalidate with caching, dedup, revalidation on focus/reconnect. An array key is normalized into the same key space as useQuery, so useSWR(['/api/user', 7], ...) and useQuery({ queryKey: ['/api/user', 7] }) share one cache entry that getQueryData and invalidateQueries both reach. The fetcher still receives the original key, so useSWR(['/api/user', id], ([url, id]) => ...) is unchanged |
useQuery(options) | Full query management: retry, staleTime, cacheTime, refetchInterval, select, placeholderData, and enabled. enabled takes a boolean, a signal or a thunk and is read reactively, so a query disabled at mount starts the moment the flag flips. Disabled with nothing cached it reports status() === 'idle' (isIdle(), and isLoading() is false); refetch() is never gated by enabled and never answered from the staleTime window. queryKey is the opposite: it is read exactly ONCE, when the hook is created, so every value in it has to be known by then. Gate on a value the key does not contain with enabled; when the key itself depends on data still loading, render the dependent component conditionally and pass the id in as a prop instead of writing ['posts', user.data()?.id], which freezes as 'posts:undefined' |
useInfiniteQuery(options) | Infinite scroll/pagination with getNextPageParam, fetchNextPage. Honours enabled (same rules as useQuery), select, retry, retryDelay and onSuccess/onError/onSettled, and exposes error(), status(), isLoading(), isError(), isSuccess(), isIdle(), isFetching(). Its data() is the page container { pages, pageParams }, not a flat array. On an EMPTY list fetchNextPage() and fetchPreviousPage() fetch the first page from initialPageParam without consulting getNextPageParam/getPreviousPageParam at all, so either can start a list created with enabled: false or retry a first page that failed; on a loaded list a getNextPageParam that returns undefined is still a no-op. Its pages live in hook-local signals, so getQueryData, setQueryData, staleTime, cacheTime, placeholderData, refetchOnWindowFocus and refetchInterval do not reach it; invalidateQueries and clearCache do |
invalidateQueries(keyOrPredicate, opts) | Trigger refetch. An array key matches as a prefix on segment boundaries, so ['todos'] also invalidates ['todos', 1]. Pass { exact: true } for that key alone, { hard: true } to clear data immediately, or a predicate function to select keys. A predicate is handed the NORMALIZED key, so an array key arrives joined (['/api/posts', 1] as '/api/posts:1') and key => key.startsWith('/api/posts') is safe; a non-string non-array key (a number, an object) is passed through unchanged. It is synchronous and returns nothing, so awaiting it does not wait for the refetches. Every form reaches infinite queries and queries that have subscribed but not yet resolved. A query whose enabled is false is deliberately left asleep |
prefetchQuery(key, fetcher) | Pre-fill cache before component mounts |
setQueryData(key, updater) | Manually set cache data |
getQueryData(key) | Read cached data |
clearCache() | Empty every cached entry, including the pages of an infinite query. A key a mounted component is still reading is emptied in place, so the clear shows on screen and a later write to that key (setQueryData, a sibling's fetch, prefetchQuery) still reaches the component. Afterwards getQueryData() reports the key as absent, and a query left with nothing settles on status() === 'idle' whether it was created enabled or disabled. It also aborts requests already in flight, including one started by an explicit refetch(), so a response that lands after the clear is discarded and await q.refetch() raced by a logout resolves with undefined |
Form state management and validation. Learn more →
| Function | Description |
useForm(options) | Complete form management: register, handleSubmit, validate, formState (values, errors, error(name), touched, isDirty, isValid, isValidating, isSubmitting, isSubmitted, submitCount, dirtyFields) |
useField(name, opts) | Individual field control: value(), error(), inputProps(). Both inputProps() and register() bind the input's value ONE WAY today: typing updates form state, but a programmatic setValue() or reset() moves the state without moving the DOM input, so a form that writes its own fields has to set el.value itself |
zodResolver(schema) | Zod validation schema adapter |
yupResolver(schema) | Yup validation schema adapter |
rules.required() | Built-in: required, minLength, maxLength, min, max, pattern, email, url, match, custom |
Input / Textarea / Select | Pre-built controlled form components |
Checkbox / Radio | Pre-built controlled toggle components. Radios in a group share one field holding the selected option, so every <Radio> needs its own value |
ErrorMessage({ name, formState, errors, render }) | Display a field's validation error. It looks the error up in what you hand it, in this order: formState.error(name) (the useForm() shape), then formState.errors[name], then errors[name] (an object or an accessor). There is no form-context lookup, so ErrorMessage({ name }) on its own resolves to null and renders nothing even when the field has an error. It resolves ONCE, when it is created, like any component: to show an error that arrives later, put it behind a thunk that reads the error, e.g. () => form.formState.error('email') && h(ErrorMessage, { name: 'email', formState: form.formState }). Default output is <span class="what-error" role="alert">; render({ message, type }) replaces it |
Stores
Global state management. Learn more →
| Function | Description |
createStore(definition) | Reactive store from a flat definition object: plain values become state, derived(state => ...) functions become computeds, any other function becomes an action bound to the state proxy. Returns a useStore() hook |
derived(fn) | Mark a function as computed inside createStore |
Animation
Physics and time-based animation primitives. Learn more →
| Function | Description |
spring(initial, opts) | Physics-based spring. Options: stiffness, damping, mass, precision |
tween(from, to, opts) | Time-based easing. Options: duration, easing, onUpdate, onComplete |
easings | Built-in: linear, easeInQuad, easeOutQuad, easeInOutCubic, easeOutBounce, etc. |
useTransition(opts) | Animate state transitions: isTransitioning(), progress(), start() |
useGesture(el, handlers) | Multi-touch: onDragStart, onDrag, onDragEnd, onPinch, onSwipe, onTap, onLongPress, plus a boolean preventDefault. Returns the live gesture state: isDragging, currentX, currentY, deltaX, deltaY, velocity as signals (startX/startY are plain numbers rewritten at gesture start) |
useAnimatedValue(initial) | React Native-like: spring(), timing(), interpolate() |
createTransitionClasses(name) | CSS transition class name generator |
cssTransition(el, name, type, ms) | Drive one of those class sets on an element: start class, forced reflow, active class, and after ms the done class. Returns a promise that resolves once the done class has landed |
Accessibility
Focus management, ARIA helpers, and keyboard navigation. Learn more →
| Function | Description |
useFocus() | Track focused element: current(), focus(el), blur() |
useFocusRestore() | Capture and restore focus from trigger elements |
useFocusTrap(ref) | Trap focus in container: activate(), deactivate() |
FocusTrap({ active }) | Component wrapper that traps focus |
announce(msg, opts) | Screen reader announcement: priority, timeout |
useAriaExpanded() | Toggle expanded state: buttonProps(), panelProps(). Both return accessor-valued props, so the documented <button {...buttonProps()}> spread keeps aria-expanded in sync as the state changes |
useAriaSelected() | Selection state: itemProps(value), accessor-valued like the rest |
useAriaChecked() | Checkbox state: checkboxProps(), accessor-valued. It also sets role="checkbox", and whichever role is applied LAST wins. On the h() path that is object order: { ...checkboxProps(), role: 'switch' } comes out a switch, { role: 'switch', ...checkboxProps() } comes out a checkbox. In compiled JSX a LITERAL attribute is baked into the cloned template and the spread runs over it afterwards, so neither <div {...checkboxProps()} role="switch"> nor <div role="switch" {...checkboxProps()}> is a switch; a dynamic role={r} written after the spread compiles to a setProp that runs after it and does win |
useRovingTabIndex(count, opts) | Keyboard navigation: getItemProps(i), containerProps(overrides), focusItem(i), setFocusIndex(i), focusIndex(). Arrow/Home/End move real DOM focus and keep exactly one item tabbable. No container role is emitted, pass your own per hook ({ role: 'toolbar' }) or per call (containerProps({ role: 'toolbar' })) |
SkipLink / VisuallyHidden | Skip navigation and screen-reader-only content |
useId() / useDescribedBy() | Unique IDs and ARIA attribute linking |
Keys / onKey() / onKeys() | Keyboard event constants and handlers |
DOM Scheduling
| Function | Description |
scheduleRead(fn) | Queue DOM read. Reads run before writes. Returns cancel function |
scheduleWrite(fn) | Queue DOM write. Returns cancel function |
measure(fn) | Promise-based read: await measure(() => el.offsetHeight) |
mutate(fn) | Promise-based write: await mutate(() => { el.style.height = '100px' }) |
nextFrame() | Promise resolving on next animation frame |
onResize(el, cb) | ResizeObserver helper. Returns unobserve function |
onIntersect(el, cb, opts) | IntersectionObserver helper. Returns disconnect function |
smoothScrollTo(el, opts) | Smooth scroll with easing |
Head Management
| Function | Description |
<Head title meta link> | Set document head tags from any component. Auto-deduplicates. On the server the tags go into the render context's head sink, and renderToString returns the body only: use renderToStringWithHead or renderDocument to get them out |
clearHead() | Remove all What-managed head tags |
Helpers & Utilities
| Function | Description |
cls(...args) | Conditional class builder: cls('btn', { active: true }) |
style(obj) | Convert style object to CSS string (for SSR) |
debounce(fn, ms) | Debounce function calls |
throttle(fn, ms) | Throttle function calls |
useMediaQuery(query) | Reactive media query matching. Returns signal |
useLocalStorage(key, init) | Synced localStorage signal |
useClickOutside(ref, handler) | Detect clicks outside element |
Server-Side Rendering
From what-framework/server, except hydrate, which is the client half and comes from what-framework. Learn more →
Every element below has to come from h() or the automatic JSX runtime
The What compiler lowers JSX to a module-level _$template() call, and that call runs document.createElement at MODULE LOAD TIME. So a compiled module cannot be imported in Node at all: the import itself throws ReferenceError: document is not defined, before any render function is reached. Nothing on this page can change that, because the failure happens before your code runs.
A server-rendered app therefore writes its components with h(), or with the automatic JSX runtime (jsx/jsxs, which build vnodes at call time rather than templates at load time), and leaves the template compiler for client-only bundles. The full-stack scaffold create-what generates is buildless and uses h() for exactly this reason. hydrate() has the same constraint on the client half: it reuses the server DOM for an h() tree, and a compiled tree cannot have produced that HTML in the first place.
| Function | Description |
renderToString(element) | Synchronous render to HTML string. Body only: any <Head> tags are dropped |
renderToStringWithHead(element) | Same render, returning { body, head } with the collected head as HTML |
renderToHydratableString(element) | Render with hydration markers so the client can reuse the server DOM |
renderToStringAsync(element, ctx) | Render, await whatever suspended (a createResource fetch), then render the whole tree again, up to 12 passes. Returns { body, head, loaderData, resources, ctx }. It resolves a createResource only when the resource was given an explicit key: an unkeyed one is auto-named per pass (__r0, __r1, …), never matches what the previous pass cached, and burns all 12 passes with the Suspense fallback still in the body. Component bodies re-run once per pass. A suspension with no <Suspense> above it is not contained: the returned promise rejects with the thrown promise |
renderToStream(element) | Async generator for streaming SSR. The only entry point that resolves a suspended lazy() component |
renderDocument(pageModule, reqCtx, opts) | The full-stack entry: runs the page's loader, renders through renderToStringAsync, and returns a complete HTML document with the collected head and one <script id="__what_data"> hydration payload (loaderData is null there when the module has no loader). pageModule may be { default, loader } or a bare component, and reqCtx.params are spread onto it as props beside loaderData. Because it goes through renderToStringAsync, the keyed-resource rule above and the lazy() limitation both apply, and the body is plain renderToString output carrying no hydration markers (renderToHydratableString is the entry point that emits those). Options: clientEntry (the only thing that emits a client <script type="module">), lang, bodyClass, head, csrfToken |
generateStaticPage(page, data) | Build-time HTML for a definePage() config. The page component renders inside a component frame, so hooks in its own body (useState, useEffect, onMount, a root Context.Provider) work |
hydrate(element, container) | Client counterpart to mount(). Reuses the server-rendered DOM instead of clearing it |
definePage(config) | Page config: mode (static/server/client/hybrid), title, meta, islands |
server(Component) | Mark component as server-only (no JS shipped) |
Server Actions
From what-framework/server.
| Function | Description |
action(fn, options) | Define server action callable from client via fetch. Options: id, onSuccess, onError, revalidate (paths), revalidateTags (cache tags purged through the bound engine after the action succeeds), timeout (client fetch timeout in ms, default 30000) |
formAction(actionFn, opts) | Form submission wrapper with FormData handling |
useAction(actionFn) | Action state: trigger(), isPending(), error(), data(), reset() |
useMutation(mutationFn, opts) | Simple mutation: mutate(), isPending(), error(), data(), reset() |
useOptimistic(initial, reducer) | Optimistic updates: value(), isPending(), addOptimistic(), resolve(), rollback(), withOptimistic(), set() |
generateCsrfToken() | Generate CSRF token for server actions |
Router
From what-framework/router.
| Function | Description |
Router({ routes, fallback }) | Main router component with route config array |
Link({ href, activeClass }) | Navigation link with automatic active class |
navigate(to, opts) | Programmatic navigation. Options: replace, state, transition |
useRoute() | Reactive route state: path(), params(), query(), hash() |
guard(check, fallback) | Route guard HOC for auth/permissions |
defineRoutes(config) | Convert route config object to route array |
Redirect({ to }) | Redirect component |
enableScrollRestoration() | Restore scroll position on navigation |
Quick Reference
import {
signal, computed, effect, batch, untrack, flushSync,
h, mount, hydrate, Fragment, html,
Show, For, Switch, Match, Portal, ErrorBoundary, Suspense,
memo, lazy, Island,
useState, useSignal, useComputed, useEffect, useMemo,
useCallback, useRef, useReducer,
onMount, onCleanup, createResource,
createContext, useContext,
createStore, derived,
Head, clearHead,
cls, style, debounce, throttle,
useMediaQuery, useLocalStorage, useClickOutside,
} from 'what-framework';
import {
useFetch, useSWR, useQuery, useInfiniteQuery,
invalidateQueries, prefetchQuery, setQueryData,
getQueryData, clearCache,
} from 'what-framework';
import {
useForm, useField, rules,
zodResolver, yupResolver, simpleResolver,
Input, Textarea, Select, Checkbox, Radio, ErrorMessage,
} from 'what-framework';
import {
spring, tween, easings,
useTransition, useGesture, useAnimatedValue,
} from 'what-framework';
import {
useFocus, useFocusRestore, useFocusTrap, FocusTrap,
announce, announceAssertive, LiveRegion,
useAriaExpanded, useAriaSelected, useAriaChecked,
useRovingTabIndex, SkipLink, VisuallyHidden,
useId, useDescribedBy, useLabelledBy,
Keys, onKey, onKeys,
} from 'what-framework';
import {
Router, Link, navigate, useRoute,
guard, defineRoutes, Redirect,
} from 'what-framework/router';
import {
renderToString, renderToStringWithHead,
renderToHydratableString, renderToStringAsync,
renderToStream, renderDocument, generateStaticPage,
definePage, server,
action, formAction, useAction, useMutation, useOptimistic,
generateCsrfToken,
} from 'what-framework/server';