React Router Tutorial
This guide walks you through integrating Composify into a React Router project. We assume you already have a React Router project set up. If not, follow the React Router getting started guide first.
Before we begin, make sure your mock API is running on port 9000. (See Prerequisites for setup instructions.) We'll use this API to store and retrieve page content.
1. Install Composify
Install Composify using your preferred package manager:
npm install @composify/react --save2. Your Existing Components
You can use plain HTML elements, but Composify works best with your own components. Let's say you have these components in your project:
/* app/components/Heading.tsx */
import type { FC, JSX, PropsWithChildren } from 'react';
import { tv, type VariantProps } from 'tailwind-variants';
const variants = tv({
base: ['margin-0', 'text-foreground', 'leading-tight', 'tracking-tight'],
variants: {
size: {
lg: 'text-lg',
xl: 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
'5xl': 'text-5xl',
},
weight: {
semibold: 'font-semibold',
bold: 'font-bold',
extrabold: 'font-extrabold',
},
align: {
left: 'text-left',
center: 'text-center',
right: 'text-right',
},
},
defaultVariants: {
weight: 'bold',
align: 'left',
},
});
type Props = PropsWithChildren<
{
level: number;
} & VariantProps<typeof variants>
>;
export const Heading: FC<Props> = ({ level, size, weight, align, ...props }) => {
const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements;
return <HeadingTag className={variants({ size, weight, align })} {...props} />;
};3. Create the Catalog
Composify needs to know which components it can work with. The Catalog maps your components to the visual editor: prop controls, default values, and categories. This keeps editor metadata separate from your component code.
/* app/components/catalog.tsx */
import { Catalog } from '@composify/react/renderer';
import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon } from 'lucide-react';
/* ... */
Catalog.register('Heading', {
component: Heading,
category: 'Content',
props: {
level: {
label: 'Level',
type: 'select',
options: [
{ label: '1', value: 1 },
{ label: '2', value: 2 },
{ label: '3', value: 3 },
{ label: '4', value: 4 },
{ label: '5', value: 5 },
{ label: '6', value: 6 },
],
default: 1,
},
size: {
label: 'Size',
type: 'select',
options: [
{ label: 'Large', value: 'lg' },
{ label: 'Extra Large', value: 'xl' },
{ label: '2XL', value: '2xl' },
{ label: '3XL', value: '3xl' },
{ label: '4XL', value: '4xl' },
{ label: '5XL', value: '5xl' },
],
default: '3xl',
},
weight: {
label: 'Font Weight',
type: 'select',
options: [
{ label: 'Semibold', value: 'semibold' },
{ label: 'Bold', value: 'bold' },
{ label: 'Extrabold', value: 'extrabold' },
],
default: 'bold',
},
align: {
label: 'Text Align',
type: 'radio',
options: [
{ label: <AlignLeftIcon />, value: 'left' },
{ label: <AlignCenterIcon />, value: 'center' },
{ label: <AlignRightIcon />, value: 'right' },
],
default: 'left',
},
children: {
label: 'Text',
type: 'textarea',
default: 'Heading',
},
},
});
/* ... */4. Render Pages
Now let's create a dynamic route at app/routes/page.tsx to render pages. This fetches the layout from the API and passes it to <Renderer />.
import '~/components/catalog';
import { Renderer } from '@composify/react/renderer';
import { type LoaderFunctionArgs, useLoaderData } from 'react-router';
export async function loader({ params }: LoaderFunctionArgs) {
const slug = params.slug ?? '';
const res = await fetch(`http://localhost:9000/documents/${slug}`);
const { content } = await res.json().catch(() => ({}));
if (!content) {
throw new Response('', { status: 404 });
}
return { slug, content };
}
export default function Page() {
const { slug, content } = useLoaderData<typeof loader>();
return (
<main className="p-4">
<section className="flex items-end justify-between mb-4">
<h1 className="text-2xl">Rendering page {slug}</h1>
<a href={`/editor/${slug}`} className="text-blue-500 hover:underline">
Visit Editor
</a>
</section>
<section className="border rounded-sm border-neutral-200">
<Renderer source={content} />
</section>
</main>
);
}Make sure to import the catalog at the top of your page so that registration happens when the page loads.
import '~/components/catalog';
import { Renderer } from '@composify/react/renderer';
import { type LoaderFunctionArgs, useLoaderData } from 'react-router';
/* ... */Register the route in app/routes.ts:
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
route(':slug', 'routes/page.tsx'),
] satisfies RouteConfig;http://localhost:5173/foo: displays the saved contenthttp://localhost:5173/baz: returns 404 (no data yet)
5. Visual Editor
Finally, create the editor page at app/routes/editor.tsx. This is where users can drag, drop, and configure your components.
import '~/components/catalog';
import '@composify/react/style.css';
import { Editor } from '@composify/react/editor';
import { type LoaderFunctionArgs, useLoaderData } from 'react-router';
export async function loader({ params }: LoaderFunctionArgs) {
const slug = params.slug ?? '';
const res = await fetch(`http://localhost:9000/documents/${slug}`);
const { content } = await res.json().catch(() => ({}));
return {
slug,
content: content ?? '<VStack />',
};
}
export default function EditorPage() {
const { slug, content } = useLoaderData<typeof loader>();
const handleSubmit = async (value: string) => {
await fetch(`http://localhost:9000/documents/${slug}`, {
method: 'DELETE',
}).catch(() => null);
await fetch('http://localhost:9000/documents', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: slug,
content: value,
}),
});
alert('Saved!');
};
return <Editor title={`Editing: ${slug}`} source={content} onSubmit={handleSubmit} />;
}Don't forget to register the editor route in app/routes.ts:
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
route('editor/:slug', 'routes/editor.tsx'),
route(':slug', 'routes/page.tsx'),
] satisfies RouteConfig;A couple things to note:
@composify/react/style.cssis required. It contains the editor's core styles.- The catalog import (
~/components/catalog) ensures your components are available in the editor.
Try It Out
- Open
http://localhost:5173/editor/foo, make some changes, and hit Save. - Visit
http://localhost:5173/footo see your changes rendered. - Open
http://localhost:5173/editor/baz, build a new page from scratch, and save it. - Visit
http://localhost:5173/baz. The page that was 404 is now live.
Wrapping Up
That's the basics covered. You now have:
- A document store (json-server for now, swap in a real database for production)
- An editor for visually composing pages with your own components
- A renderer that turns stored JSX into actual UI
Next steps:
- Replace json-server with a proper database
- Add authentication
- Deploy so your team can use it
If you don't want to build the backend yourself, Composify Cloud handles storage, version history, and collaboration out of the box.