@webflow/react
v2.1.1
Published
The core React integration package for building Webflow code components. This package provides the essential tools for declaring components, rendering them on both client and server, and accessing Webflow-specific context.
Maintainers
Keywords
Readme
@webflow/react
The core React integration package for building Webflow code components. This package provides the essential tools for declaring components, rendering them on both client and server, and accessing Webflow-specific context.
Installation
npm i @webflow/reactPeer Dependencies
This package requires the following peer dependencies:
npm i react react-domUsage
Declaring Components
Use declareComponent to create a Webflow code component definition. This should be the default export from your *.webflow.tsx file.
Basic Example
import { declareComponent } from "@webflow/react";
import { props } from "@webflow/data-types";
function Button({ text, link }) {
return (
<a href={link?.href} target={link?.target}>
{text}
</a>
);
}
export default declareComponent(Button, {
name: "Button",
description: "A customizable button component",
props: {
text: props.Text({ name: "Text", defaultValue: "Click me" }),
link: props.Link({ name: "Link" }),
},
});With Decorators
Decorators allow you to wrap your component with additional functionality, such as CSS-in-JS providers:
import { declareComponent } from "@webflow/react";
import { emotionShadowDomDecorator } from "@webflow/emotion-utils";
export default declareComponent(MyComponent, {
name: "My Component",
decorators: [emotionShadowDomDecorator],
});With Options
export default declareComponent(MyComponent, {
name: "My Component",
props: {
// ... your props
},
options: {
applyTagSelectors: true, // Provide styles targeting tag selectors (default: false)
ssr: "prerender", // Enable server-side rendering awaiting Suspense boundaries (default: true)
},
});Using Webflow Context
Access Webflow-specific context data in your components using the useWebflowContext hook:
import { useWebflowContext } from "@webflow/react";
function MyComponent() {
const { mode, interactive, locale } = useWebflowContext();
return (
<div>
<p>Mode: {mode}</p>
<p>Interactive: {interactive ? "Yes" : "No"}</p>
<p>Locale: {locale}</p>
</div>
);
}Context Properties:
mode- The current mode ("design"or"preview")interactive- Whether the component is in an interactive statelocale- The current locale (e.g.,"en-US")
Prerender data and hydration
Three hooks make async data resolved during prerender available synchronously when the Webflow runtime hydrates the component (via options.data). Values must be JSON-serializable. Each read hook returns an object — destructure it: const { data } = useSuspenseData(...) / usePrerenderData(...).
useSuspenseData(key, loader)— the hook owns the fetch. Use it for components that fetch their own data (no external data library). The loader runs and suspends during prerender, the result is recorded underkey, and the recorded value is returned synchronously on the client. Returns{ data }.usePrerenderData<T>(key)— read-only and never fetches or suspends. Use it for components whose fetching is owned by a Suspense-capable library (React QueryuseSuspenseQuery, SWR{ suspense: true }). It returns{ data }, the prerendered value (orundefined). Feeddataback to the library asinitialData/fallbackData, then calluseHydrateData(key, value)— with the samekey— to record the library's resolved value. Define the key once and reuse it forusePrerenderData, your library's query key, anduseHydrateData.
Shared rules:
- Not a general client cache: there is no built-in invalidation or refetch. Use normal React patterns (or a data library) for updates after the first paint.
- Use a stable key per logical resource — either a plain string (used as-is, e.g. a URL + query string like
useSuspenseData("/cities?offset=20", loader)) or an array ofstring,number, orbooleansegments (e.g.useSuspenseData(["todo", todoId], loader)). Array segments serialize to colon-joined strings (["t", "x"]→"t:x"inServerPrerenderResult.data). This is a cache identity, not a React deps array. First-write-wins if keys collide (applies to bothuseSuspenseData's auto-record anduseHydrateData). - The Webflow renderer wraps every code component in a host
SuspenseBoundary(alongsideErrorBoundary), so you do not need your own<Suspense>for these hooks to work. Add a customer<Suspense>only when you want a custom loading UI. - If a suspend is caught only by the host boundary (no customer fallback), SSR may emit an HTML comment (
<!-- webflow-cc:suspense-fallback ... -->) for debugging — usessr: 'prerender'withuseSuspenseData/usePrerenderDataso data resolves before paint. - With
options.ssr: false/ no prerender,useSuspenseDatastill runs theloaderon the client (Suspense). Seeded values (data[key]from prerender) only apply when the component tree is underPrerenderDataProviderwith thatdataobject (seeClientRenderer.hydrateandClientRenderer.renderbelow). ClientRenderer.hydrateandClientRenderer.renderwrap the subtree inPrerenderDataProviderwithmode="seeded"anddata={options?.data ?? {}}. Ifdatacontainskey,useSuspenseData/usePrerenderDatareturn it synchronously. OtherwiseuseSuspenseData'sloaderruns (Suspense) with a per-provider keyed cache (samekeyunder one provider reuses oneloader(); firstloaderwins if the samekeyis used with different loaders). OutsidePrerenderDataProvider, there is no keyed cache.ClientRenderer.mountonly creates aReactDOM.Root; it does not wrap withPrerenderDataProvideror render content.- Pass
options.dataon eachrenderwhen prerender seeds must stay available (each call replaces the root tree;{}means no seeds for that update). - Prerender uses
PrerenderDataProviderwithmode="collect"and a mutabledataobject;ServerRenderer.prerenderToStringwrites loader / hydrate results intodataand returns them asServerPrerenderResult.data.
ServerRenderer.prerenderToString always resolves to a ServerPrerenderResult: { html, styles?, data? } (see @webflow/data-types). The Webflow host is responsible for persisting data onto the page and passing it back into ClientRenderer.hydrate and ClientRenderer.render (e.g. { ..., data } on each update that should keep seeds).
Fetching your own data (useSuspenseData)
For components that fetch their own data (no external data library), use useSuspenseData. It owns the whole flow: during prerender it runs the loader, suspends until it resolves, and records the result under key; on the client that recorded value is returned synchronously, so the component paints with real content on first render — no refetch, no loading flash. You don't call useHydrateData here; useSuspenseData records automatically.
import { props } from "@webflow/data-types";
import { declareComponent, useSuspenseData } from "@webflow/react";
type Profile = { name: string; bio: string };
async function fetchProfile(id: string): Promise<Profile> {
const res = await fetch(`/api/profiles/${id}`);
return res.json();
}
function ProfileCard({ id }: { id: string }) {
// Array key (joined with ":") — or a string like `/api/profiles/${id}`.
const { data } = useSuspenseData<Profile>(["profile", id], () =>
fetchProfile(id)
);
return (
<article>
<h1>{data.name}</h1>
<p>{data.bio}</p>
</article>
);
}
export default declareComponent(ProfileCard, {
name: "Profile Card",
props: { id: props.Text({ name: "Profile ID", defaultValue: "42" }) },
// Resolve the loader during prerender so the data is ready on first paint.
options: { ssr: "prerender" },
});Notes:
- The resolved value must be JSON-serializable and the
loadershould be pure for a given key — it may run more than once (e.g. under React Strict Mode), and first-write-wins on key collision. - The host already wraps the component in a
SuspenseBoundary, so no<Suspense>is needed; add one only for a custom loading UI. - Unlike the data-library form, loader errors are serialized into
ServerPrerenderResult.dataduring prerender (prerender still resolves; the hostErrorBoundaryrenders empty for that subtree) and rethrown on the client from the seed so they surface consistently through the hostErrorBoundary.
Using a data library (React Query / SWR)
usePrerenderData (read-only) lets a Suspense-mode data library participate in prerender hydration without double-fetching. The library keeps ownership of fetching, caching, and refetch. Pair it with useHydrateData to record the library's resolved value.
// React Query — define the key once and reuse it everywhere
const key = ["cities", offset];
const { data: initialData } = usePrerenderData<CitiesPage>(key);
const { data } = useSuspenseQuery({
queryKey: key,
queryFn: () => fetchCitiesPage(offset),
initialData,
});
useHydrateData(key, data);// SWR
const key = ["cities", offset];
const { data: fallbackData } = usePrerenderData<CitiesPage>(key);
const { data } = useSWR(key, () => fetchCitiesPage(offset), {
suspense: true,
fallbackData,
});
useHydrateData(key, data); // SWR types `data` as `T | undefined`; useHydrateData accepts that (records nothing if undefined)The key is a plain string or array, so the same value doubles as your library's query key — no separate handle to thread, and no chance of the prerender key drifting from the library key.
Requirements and caveats when using a data library:
- The library must be in Suspense mode during prerender (
useSuspenseQuery, SWR{ suspense: true }).ssr: 'prerender'only awaits Suspense; a non-suspense library resolves via state/effects that prerender never awaits, anduseHydrateDatawould recordundefined. useHydrateDatais a hook: call it at the top level during render, not inuseEffect— effects don't run during prerender, so the value wouldn't be captured. (The rules of hooks enforce this placement.)usePrerenderDatadoes not transport errors: a library rejection during prerender surfaces through the hostErrorBoundary, while on the client the library re-fetches and owns its own error UI. (useSuspenseDataserializes loader errors and rethrows them on the client.)- With
useSuspenseQuery+initialData(or SWR +fallbackData), the first client render is synchronous (no suspense, no hydration mismatch); the library then revalidates in the background per its own config (staleTime, etc.).
Server-Side Rendering
This package provides a server-side renderer for React components. The ServerRenderer provides:
- Server-side rendering with
renderToStringandrenderToStream prerenderToString— awaits Suspense boundaries (onAllReady) and returns{ html, data? }(plusstyles?when using Emotion/styled-components server packages)- Support for creating slot elements with
createSlot - Automatic handling of Webflow context during SSR
Note: For CSS-in-JS libraries like Emotion or styled-components, use their respective server renderers instead:
@webflow/emotion-utils/server@webflow/styled-components-utils/server
Configure the server renderer in your webflow.json file:
{
"library": {
"renderer": {
"server": "@webflow/emotion-utils/server"
}
}
}API Reference
declareComponent
Creates a Webflow code component definition.
Type:
<P extends {}>(
Component: React.ComponentType<P>,
data: ComponentData<P, React.ReactNode, React.ComponentType<P>>
) => ComponentDefinition<React.ComponentType<P>, React.ReactNode, P>;Parameters:
Component- The React component to renderdata- Component metadata and configurationdata.name- The display name of the componentdata.description(optional) - Description of the componentdata.group(optional) - Group for organizing componentsdata.props(optional) - Component props configurationdata.options(optional) - Additional optionsdata.options.applyTagSelectors(optional) - Provide tag selector styles (default:false)data.options.ssr(optional) - Enable server-side rendering (default:true)
data.decorators(optional) - Array of decorator functions
Returns: A Webflow code component definition
useSuspenseData
useSuspenseData(key, loader) — see Fetching your own data.
Type:
// `Key` shorthand below: a plain string is used as-is (handy for URLs); an array is joined
// with ":". Define it once and reuse it. Inferred from your argument — there's no exported
// type to import.
type Key = string | readonly (string | number | boolean)[];
function useSuspenseData<T>(key: Key, loader: () => Promise<T>): { data: T };usePrerenderData
usePrerenderData<T>(key) — read-only seed for data libraries. See Using a data library.
Type:
function usePrerenderData<T>(key: Key): { data: T | undefined };useHydrateData
useHydrateData(key, value) — records a value fetched by your own Suspense-capable data library into the prerender snapshot. Pair it with usePrerenderData(key), reusing the same key. See Using a data library.
Type:
function useHydrateData<T>(key: Key, value: T | undefined): void;Pass the same key you gave usePrerenderData and your data library — define it once and reuse it so it can't drift. value accepts undefined (a no-op), so libraries whose data stays T | undefined (e.g. SWR) need no cast. Call it during render — it's a hook, so the rules of hooks keep it out of effects and conditionals.
useWebflowContext
Hook to access Webflow context data.
Type:
() => WebflowContextType;Returns:
{
mode: "design" | "preview";
interactive: boolean;
locale: string;
}ClientRenderer
A factory that creates a client-side renderer for a React component.
Type:
ComponentClientRendererFactory<
React.ComponentType<ComponentRuntimeProps<React.ReactNode>>,
ReactDOM.Root,
React.ReactNode
>;Methods:
mount(domNode)- Creates aReactDOM.Rooton the DOM node only (noPrerenderDataProvider, no initial render).hydrate(domNode, props?, options?)- Hydrates a server-rendered tree wrapped inPrerenderDataProviderwithmode="seeded"anddatafromoptions?.data ?? {}(replays prerenderdataforuseSuspenseData/usePrerenderData).render(root, props?, options?)- Renders to an existing root wrapped inPrerenderDataProviderwithmode="seeded"anddatafromoptions?.data ?? {}(same ashydratefor prerender-data context). Passoptions.dataon each call when seeds must remain available.createSlot(name)- Creates a named slot element for component composition
ServerRenderer
A factory that creates a server-side renderer for a React component.
Type:
ComponentServerRendererFactory<
React.ComponentType<ComponentRuntimeProps<React.ReactNode>>,
PipeableStream,
ReactDOMServer.RenderToPipeableStreamOptions,
React.ReactNode,
ReactDOMServer.ServerOptions
>;Methods:
renderToStream(props?, options?, streamOptions?)- Renders component to a pipeable streamrenderToString(props?, options?, stringOptions?)- Renders component to a stringprerenderToString(props?, options?, prerenderOptions?)- Prerender after Suspense resolves; returnsPromise<ServerPrerenderResult>createElement(props?, options?)- Creates a React element with the componentcreateSlot(name)- Creates a named slot element for component composition
applyDecorators
Utility function to apply an array of decorators to a component.
Type:
<P extends {}>(
Component: React.ComponentType<P>,
decorators: Array<
(Component: React.ComponentType<P>) => React.ComponentType<P>
>
) => React.ComponentType<P>;License
MIT
