Farm.js

Islands and islx

Islands mode renders server components without React's Flight protocol. Farm uses islx as its wire format end to end: the HTML a page sends, the props and values its client components receive, its server actions, and its client navigations. You write the same server and client components as for React Server Components.

import { defineConfig } from "@farm.js/core";

export default defineConfig({
  experimental: { serverComponents: { mode: "islands" }, serverActions: true },
});

What replaces Flight

With { mode: "rsc" }, the server renders a Flight payload (react-server-dom-webpack), and the browser rebuilds the whole page from it. With { mode: "islands" }, there is no Flight payload. The page is ordinary server HTML, and islx marks the parts the browser hydrates.

RSC (mode: "rsc")Islands (mode: "islands")
Server outputFlight payload, rendered to HTMLHTML only, with islx markers
What the browser hydratesthe whole page, as one React rooteach client component, as its own root
A client component's propsinside the Flight payload<script type="f/props"> after it
Server output passed as childreninside the Flight payloadkept as HTML in <f-slot>, or as a server tree when it holds client components
Large shared valuesinside the Flight payloadsent once, keyed by content (f/value); a navigation leaves out values the browser holds (F-Values)
Server actionsFlight action referencesislx references, captured values sealed with AES-GCM
Client navigationa Flight payload for the pagethe page's HTML, and only the layouts that changed (F-Layouts)
Packages it needsreact-server-dom-webpack, @vitejs/plugin-rsc (pinned versions)@farm.js/islx (a dependency of @farm.js/core)

A client component's server HTML looks like this:

<f-island src="/src/components/counter.tsx" export="Counter" props="_R_a_" strategy="load">
  <button>Count: 5</button>
</f-island>
<script type="f/props" for="_R_a_" ids="_R_ba_">
  {"start":5}
</script>

strategy is when the island hydrates (load, idle, visible or interaction), and ids lets the browser give useId the ids in the HTML. The full contract is islx's SPEC.md.

How Farm uses islx

  • Server. Farm renders the page once, on its normal server pipeline. Farm's compiler wraps every "use client" export in islx's island component, which writes the <f-island> element and its props. Server output passed to a client component renders once, inside it.
  • Browser. Farm's client entry hands every <f-island> to islx's hydration runtime, which imports the island's module when its strategy is due and hydrates it in place. A page whose islands all wait loads a 1.7 KB boot script instead of the runtime.
  • Layouts. A layout that needs one React root (a client provider around the pages) renders as an islx server tree: the browser rebuilds it from the HTML, so its module stays on the server, and a navigation keeps the provider's state.
  • Actions. "use server" functions compile to islx action references. A form posts its action without a page load, or without JavaScript, and the page refreshes with its islands' state kept.
  • Navigation. A navigation fetches the page's HTML, streamed, with only the layouts the browser does not keep, and hydrates the islands it brings.

Libraries written for React Server Components

Islands mode runs libraries written for RSC the way their docs show it. Farm shims the RSC-only parts:

PatternWorksHow islands mode does it
A package that ships a react-server build (react-tweet's server Tweet, react-i18next, react-hook-form, swr, @apollo/client/react)yesA server module's import resolves to that build, and so do the packages it imports. Names it takes from a "use client" module stay client components. React itself keeps one build for server and client components.
import "server-only" in server codeyesServer builds get an empty module. A browser build that imports it fails, naming the importer.
import "client-only"yesA client component gets its empty build; a server component gets its react-server build, which throws, as RSC rejects it.
A package client component in a server component (next-themes, sonner's Toaster, Radix parts, jotai's Provider, SWRConfig)yesFarm writes a client reference module for each export (.farm/client-references/) and server code imports that, so the export renders as an island, as RSC renders a client reference.
A package provider around server contentyesClient components inside it are its descendants and read its context.
A package provider in a layoutyesThe layout renders as a server tree; the provider keeps its state across navigations.
useFormStatus in a client component inside a server <form>yesFarm points the import at islx's useFormStatus, which reports the server form's submit.
useId in client componentsyesIds match the server HTML and are unique across islands.
cache from react in server code, or React.cacheyesFarm points them at a request-scoped cache, as RSC memoizes it, in app server code and in the react-server builds it reaches.
cacheSignal() from react in server codeyesIt returns the request's AbortSignal, which aborts when the response ends, as RSC's does after a render.
A namespace import passed around as a value (const { Close } = Dialog)yesIts components become client references; its other exports (createDialogScope) come from the package.

tests/e2e-server-components/compat.spec.ts runs these against both engines.

Known differences

  • A module both server and client components import is one module in islands mode, where RSC has a copy in each graph. Farm treats a module that any "use client" module reaches as client code, so its imports keep the packages' default builds, and a server component that uses it gets those too. Modules only server code imports get the react-server builds.
  • Package code that calls React's cache outside a react-server build keeps React's own, which calls the function every time outside RSC.
  • cacheSignal() outside a request (at module scope, in a build step) returns null. In RSC its signal aborts when the render finishes; in islands mode, when the response ends.
  • A namespace passed around as a value carries the package's non-component exports (createDialogScope) as the real functions. RSC gives client references that throw when called.
  • Package client components always hydrate, including ones that only render markup (icons), as RSC sends them as client components too. Each one is an island.