Loader Data

Pages often need data that isn’t stored in the page itself: the current locale, the signed-in user, the product for /products/[handle], or documents a block points at. Load it in your route’s loader, the same place you load the page, and pass it to React Editor as loaderData. Every component receives it next to its props.

<Render config={config} data={page} loaderData={{ locale: "fr", product }} />

loaderData is never saved to the page data. It’s passed as a prop rather than through React context, so it works the same in the client renderer and in React Server Components.

Loading it

Use your framework’s own loader, and pass the result to <Render> on published pages and <Editor> on the editor route.

Next.js (App Router)

app/products/[handle]/page.tsx
import { Render } from "@reacteditor/core/rsc";
import { config } from "@/editor.config";
 
export default async function Page({ params }) {
  const { handle } = await params;
  const [page, product] = await Promise.all([
    getPage(`/products/${handle}`),
    getProduct(handle),
  ]);
 
  return <Render config={config} data={page} loaderData={{ product }} />;
}

TanStack Start

Loaders also run in the browser on client-side navigation, so wrap server-only reads in a server function:

routes/products.$handle.tsx
const getProductPage = createServerFn({ method: "GET" })
  .inputValidator((handle: string) => handle)
  .handler(async ({ data: handle }) => ({
    page: await getPage(`/products/${handle}`),
    product: await getProduct(handle),
  }));
 
export const Route = createFileRoute("/products/$handle")({
  loader: ({ params }) => getProductPage({ data: params.handle }),
  component: () => {
    const { page, product } = Route.useLoaderData();
    return <Render config={config} data={page} loaderData={{ product }} />;
  },
});

The editor route

Load the same data for the page being edited, so blocks render the way they will once published:

<Editor config={config} data={page} loaderData={{ product }} />

Reading it

In render

const config = {
  components: {
    ProductTitle: {
      render: ({ loaderData }) => <h1>{loaderData.product?.title}</h1>,
    },
  },
};

loaderData is reserved, so don’t define a field called loaderData. React Editor warns in development if a component does.

In resolveData and resolveFields

Both receive it as params.loaderData. When you run resolveAllData on the server, pass it as the third argument:

const resolved = await resolveAllData(page, config, { product });

In field transforms

Field transforms receive loaderData too, which lets a field store a reference and the component receive what it points at. Pass the same transforms to <Editor> and <Render>:

const fieldTransforms = {
  // A "product" field stores a handle; render receives the product
  product: ({ value, loaderData }) => loaderData.products?.[value] ?? null,
};
 
<Render
  config={config}
  data={page}
  loaderData={{ products }}
  fieldTransforms={fieldTransforms}
/>;

In the editor, a plugin’s useLoaderData can fetch that data for whatever blocks are on the page while you edit.

Typing it

loaderData is typed as LoaderData, an interface you can augment:

editor.d.ts
declare module "@reacteditor/core" {
  interface LoaderData {
    locale?: string;
    product?: Product;
  }
}

Mark keys optional: not every route loads everything.

Further reading