Renderers
Choose React, Preact, Solid, Vue, or Svelte for application UI while keeping FARMJS routing, server APIs, middleware, observability, and deployment. React remains the default; Preact, Solid, Vue, and Svelte are beta renderer adapters.
Choose a renderer
| Renderer | Select it with | Route components | Best fit |
|---|---|---|---|
| React | Omit renderer | .tsx, .jsx | Complete FARMJS client API and integration UI support. |
| Preact | renderer: preact() | .tsx, .jsx | Small React-compatible runtime with streaming SSR. |
| Solid | renderer: solid() | .tsx, .jsx | Fine-grained interactive UI with FARMJS server features. |
| Vue | renderer: vue() | .vue | Vue SFCs, SSR, hydration, and FARMJS server features. |
| Svelte | renderer: svelte() | .svelte | Svelte 5 components, runes, SSR, and FARMJS server features. |
The renderer controls component compilation, element creation, server rendering, and browser hydration. FARMJS continues to control route discovery, layouts, API routes, middleware, cache and storage, integrations, observability, and deployment output.
Feature support
The first two tables are checked against the renderer descriptors and their server/client exports, so entry-point or capability changes cannot silently leave the documentation stale.
| Renderer | Vite entry | Server entry | Client entry | Route module extensions |
|---|---|---|---|---|
| React | @farm.js/core/renderer/react/vite | @farm.js/core/renderer/react/server | @farm.js/core/renderer/react/client | .ts, .tsx, .js, .jsx |
| Preact | @farm.js/preact/vite | @farm.js/preact/server | @farm.js/preact/client | .ts, .tsx, .js, .jsx |
| Solid | @farm.js/solid/vite | @farm.js/solid/server | @farm.js/solid/client | .ts, .tsx, .js, .jsx |
| Vue | @farm.js/vue/vite | @farm.js/vue/server | @farm.js/vue/client | .ts, .tsx, .js, .jsx, .vue |
| Svelte | @farm.js/svelte/vite | @farm.js/svelte/server | @farm.js/svelte/client | .ts, .tsx, .js, .jsx, .svelte |
| Renderer | SSR | Streaming | Hydration | Component head output | Route updates | Plain functions |
|---|---|---|---|---|---|---|
| React | Yes | Node (Web on edge targets) | Yes | Framework metadata | Reconciles | Yes |
| Preact | Yes | Node + Web | Yes | Framework metadata | Reconciles | Yes |
| Solid | Yes | Node + Web | Yes | Framework metadata | Remounts (why) | Yes |
| Vue | Yes | Node + Web | Yes | Framework metadata | Reconciles | Yes |
| Svelte | Yes | Buffered (test) | Yes | Native + framework | Reconciles | Yes |
“Component head output” means markup emitted from inside the renderer's component model. All five
renderers support FARMJS static metadata; Svelte additionally carries <svelte:head> output through
its server adapter. “Route updates” describes what happens when FARMJS hands an existing browser
root a freshly materialized route tree, not ordinary reactive updates inside a mounted component.
The higher-level framework features below use those adapter primitives:
| Capability | React | Preact | Solid | Vue | Svelte |
|---|---|---|---|---|---|
| File pages and nested layouts | Available | Available | Available | Available | Available |
| Loading, error, not-found, and slot files | Available | Available | Available | Available | Available |
| Static metadata and favicon configuration | Available | Available | Available | Available | Available |
| API routes and generated typed API clients | Available | Available | Available | Available | Available |
| Server functions, middleware, cache, and storage | Available | Available | Available | Available | Available |
| Observability and production Node output | Available | Available | Available | Available | Available |
| Basic create-app starter | Available | Available | Available | Available | Available |
| Better Auth create-app starter | Available | Native | Native | Native | Native |
| Router state and programmatic navigation | Available | Through preact/compat | Solid binding | Vue binding | Svelte store |
| Callable actions and server queries | Available | Through preact/compat | Solid binding | Vue binding | Svelte store |
| Client theme and i18n state | Available | Through preact/compat | Solid binding | Vue binding | Svelte store |
Renderer-specific Link and form components | Available | Through preact/compat | Not yet | Not yet | Not yet |
| Programmatic UI routes | Available | Compatibility surface | React-oriented today | React-oriented today | React-oriented today |
| Markdown/MDX visual routes and docs adapter | Available | Compatibility surface | React-oriented today | React-oriented today | React-oriented today |
| Generated JSX metadata images | Available | Compatibility surface | React-oriented today | React-oriented today | React-oriented today |
| React Server Components and optimized boundaries | Available experimentally | Not applicable | Not applicable | Not applicable | Not applicable |
| Other integration UI providers and starters | Available | Provider-specific compatibility | React-oriented today | React-oriented today | React-oriented today |
Follow the focused compatibility notes for Preact, Solid, Vue, and Svelte before choosing a non-React renderer for a React-oriented UI surface.
In experimental React Server Components, synchronous page and layout components are rendered through React, including supported server hooks such as useId(). A string or variable containing async does not make a component asynchronous. Stateful hooks and effects still belong in Client Components.
Preact resolves the React-shaped bindings through preact/compat. Solid exposes signal-backed
getters, Vue exposes refs and computed values, and Svelte exposes readable stores. The underlying
navigation, action, query-cache, theme, and i18n transports live in the renderer-neutral
@farm.js/core/renderer-client entry.
Native client bindings
Import client bindings from the selected renderer rather than importing React hooks:
// Solid
import { useAction, useRouter, useServerQuery, useTheme } from "@farm.js/solid/bindings";
// Vue
import { useAction, useRouter, useServerQuery, useTheme } from "@farm.js/vue/bindings";
// Svelte
import {
createAction,
createRouter,
createServerQuery,
createTheme,
} from "@farm.js/svelte/bindings";Actions remain normal typed RPC calls. Solid exposes action state through reactive properties, Vue through refs, and Svelte through the callable action's readable-store subscription. Server queries share FARMJS's existing browser cache, invalidation, deduplication, stale-while-revalidate, focus, and reconnect behavior across all bindings.
Renderer capability contract
Renderer packages advertise streaming support in their descriptor instead of relying on FARMJS to guess from optional runtime exports:
import { defineRenderer } from "@farm.js/core";
export const customRenderer = defineRenderer({
name: "custom",
vite: "@example/renderer/vite",
server: "@example/renderer/server",
client: "@example/renderer/client",
capabilities: {
streaming: {
node: true,
web: false,
runtimes: {
edge: { node: false, web: true },
},
},
reconcilesRerenders: false,
functionComponents: false,
},
});The top-level streaming values are the fallback when no runtime override exists and for presets
whose runtime is unknown. Local development resolves as node; production resolves from the
deployment preset. A runtimes.node or runtimes.edge entry overrides the pair once FARMJS knows
the runtime. Existing renderers that support the same primitives everywhere can keep using only
the two booleans.
| Renderer | Node target | Edge target |
|---|---|---|
| React | Node stream | Web stream |
| Preact | Node + Web streams | Node + Web streams |
| Solid | Node + Web streams | Node + Web streams |
| Vue | Node + Web streams | Node + Web streams |
| Svelte | Buffered | Buffered |
Re-render behavior
reconcilesRerenders states whether re-rendering an existing root diffs the new tree against the
live DOM or rebuilds it.
Virtual-DOM renderers (React, Preact, Vue) compare the incoming tree with what is mounted. Svelte's FARMJS compatibility root likewise applies a new element description through one mounted reactive root. In both cases, a client navigation keeps matching DOM nodes, focus, and component state.
Solid remains a compile-time fine-grained renderer: handing root.render() a freshly materialized
tree replaces its nodes because there is no virtual DOM to diff. FARMJS therefore does not use
ordinary root re-rendering for a shared Solid layout. The client runtime supplies route state to the
adapter's optional renderRoute() method instead; the Solid adapter keeps the matching layout chain
mounted and updates its page slot and params through signals. Changing the layout chain still mounts
the new chain, as it does in the other renderers.
Renderers that do not declare the field are treated as rebuilding, so nothing silently depends on reconciliation it will not get. The shared renderer conformance suite asserts the behavior each renderer declares, so the flag cannot drift away from what the adapter actually does.
Custom fine-grained renderers can implement the same optional route-update contract:
import type { FarmRendererClientRoot, FarmRendererRouteState } from "@farm.js/core/renderer";
interface CustomRoot extends FarmRendererClientRoot {
renderRoute(state: FarmRendererRouteState): void;
}
export function hydrateRoot(
container: Element,
element: unknown,
initialRoute?: FarmRendererRouteState,
): CustomRoot {
// Establish native reactive bindings from initialRoute during hydration.
// Later navigations call root.renderRoute(nextRoute).
}layouts arrive outermost first with stable route patterns, page is the innermost route slot, and
params is the current route-param snapshot. element is the fully composed fallback tree, while
wrap() reapplies framework-owned outer wrappers such as integration providers. Renderers that omit
renderRoute() continue through render(element) unchanged.
Function components
functionComponents states whether the renderer can render a plain function component: one that
takes props and returns an element tree rather than a component built by the renderer's own
compiler. FARMJS gates integration provider components on this capability.
All official renderers support synchronous plain function components. React and Preact handle them through their native element model, while Vue and Solid materialize their returned element tree in the adapter. A compile-time renderer such as Svelte also needs its adapter to distinguish a plain function from one of its own compiled components, because both are functions at runtime.
FARMJS resolves the ambiguity at build time rather than guessing. A provider component in a
production build must be an importable module reference, so the module's extension answers the
question: a .svelte provider under the Svelte renderer is a Svelte component, while a .tsx one is
a function component. The check uses the extensions the renderer itself declares in
componentExtensions, not the resolved set, which always includes .ts, .tsx, .js, and .jsx.
Components that are not renderer-compiled are marked so the adapter calls them instead of
instantiating them.
Async plain function components are not part of this contract. Official adapters reject them with
an actionable error instead of rendering a promise, an empty string, or [object Object]. Resolve
the data before creating the element tree or use the renderer's native asynchronous primitives.
The field defaults to false, so a renderer whose adapter has not been taught to handle function
components fails with a clear error naming the renderer rather than rendering something broken.
FARMJS builds the production client and SSR graphs in parallel by default. If a renderer's compiler
plugin uses process-global mutable caches, set buildConcurrency: "serial" on its descriptor. The
official Vue renderer does this because @vitejs/plugin-vue shares SFC descriptor and script caches
between plugin instances.
A renderer advertising node streaming for the active runtime must export
renderToPipeableStream() from its server entry. A renderer advertising web streaming must
export renderToReadableStream() returning a WHATWG ReadableStream. FARMJS validates those
declarations when the development server renderer starts and again in the generated production
runtime. Production still checks the function before calling it, but only a primitive enabled by
the resolved descriptor can be selected. This keeps the declaration and runtime export in
agreement instead of treating an accidental export as support. FARMJS uses buffered
renderToString() when neither capability is enabled. Descriptors without a capabilities field
remain buffered for compatibility.
Renderer-neutral server code
Keep product and server behavior outside the component runtime whenever possible:
import { createEndpoint } from "@farm.js/core";
import { createServerFn } from "@farm.js/core/server-fn";
import { z } from "zod";
const input = z.object({ name: z.string().min(1) });
const greet = createServerFn({
input,
async handler({ input }) {
return { message: `Hello, ${input.name}` };
},
});
export const POST = createEndpoint(
"/api/greeting",
{ method: "POST", body: input },
async ({ body }) => greet(body),
);React, Preact, Solid, Vue, and Svelte components can call this endpoint through the same generated client. Database access, secrets, validation, cache invalidation, middleware, and the server-function handler remain on the server.
Switch renderers deliberately
The renderer option is application-wide. Do not mix React, Preact, Solid, Vue, and Svelte route components in the same route tree. Share server modules, schemas, API clients, CSS, and plain TypeScript across renderers; rewrite component and client-state code using the selected renderer's native primitives.
The Basic and Better Auth templates support every renderer directly:
pnpm create @farm.js/app my-auth-app --template better-auth --renderer vue --typescriptOther integration starter templates currently target React. Add their renderer-neutral server integration to a native Basic starter when using Preact, Solid, Vue, or Svelte.
Choose React, Preact, Solid, Vue, or Svelte for application UI while keeping FARMJS routing, server APIs, middleware, observability, and deployment.