A CMS with React needs no custom parser to render your content
It ties content storage to React's component model, not a generic block editor. A blog post or landing page ships as JSX your app already knows how to render. Draftbase is a React CMS built on MDX. It has a typed SDK and a drop-in renderer for Next.js. No proprietary rich-text format sits between the content and the component tree.
Updated
import { cms } from "@/lib/cms/client";
import { MDXContent } from "@draftbase/renderer";
const post = await cms.getEntry<BlogPostFields>(id);
// post.fields.content is typed MDX
<MDXContent source={post.fields.content} />`getEntry` is generated from your template schema, so `post.fields.content` is a typed string, not `any`. `<MDXContent>` evaluates it and renders straight to React.
What is a React CMS
A React CMS hands content back as data a React component can render directly, instead of an HTML fragment or a proprietary block format. That matters because a React app already renders UI as a tree of components. A CMS that only outputs HTML strings forces a choice: trust that HTML with dangerouslySetInnerHTML, or write a parser that turns it back into components. Neither is built for React. Both treat it as another template target, not the real rendering layer.
Most CMS products predate React. They were built to output whole pages, not small reusable pieces. A React CMS closes that gap by storing content in a format that maps onto JSX with no translation step, usually MDX or a tree of JSON nodes, keeping content structure and component structure in sync. Fields on an entry become typed props, with full TypeScript support end to end. A rich text field becomes a component tree your renderer already knows how to walk.
There's a rendering-time win here too. MDX compiles down to real JSX, so a page built on a React CMS arrives as server-rendered markup on first paint rather than a client-side widget waiting on a script. A blog post or a docs page shows up as markup a browser or a crawler can read immediately, the same way a hand-written component would. No loading skeleton standing in for content.
This matters for react applications rendered on the server as much as ones that hydrate purely client-side, and for most frontend projects today. React remains the most-used JavaScript framework. 81.1% of developers use it. (Source) A CMS built around how React actually renders serves that majority directly, instead of treating React as one output target among several.
Two storage formats show up in practice. Some React CMS products store the component tree as JSON, an array of nodes with a type, props, and children, hand-built from a visual editor. Others, Draftbase included, store MDX: plain Markdown with embedded JSX. MDX has the edge, because it's already a spec with parsers and editor tooling behind it rather than a schema each product reinvents.
Using a headless CMS with React
A headless CMS for React keeps content behind an API and leaves rendering to your app. Your components fetch an entry and render it. No theme layer, no template engine, no page assembled on the CMS side. What varies between products is the shape the content arrives in, and that shape decides how much rendering code you end up owning.
For a React website, the field that settles it is rich text. Contentful returns a JSON node tree; Sanity returns Portable Text. Either way you write a walker that maps every node type to a component, then extend it the next time an editor wants a new block. Draftbase stores that field as MDX and renders it with <MDXContent>. Same API-first setup. No walker.
Then there's cost. Self-hosting Strapi, Payload, or Directus gives you a free and open-source headless CMS, and you pay for it in hosting, upgrades, and backups instead. Draftbase's Hobby plan is free and hosted, with paid plans from $49/mo. If you'd rather build a CMS with React yourself, read the next section first.
Building a CMS with React vs. using one
Teams building React content today usually pick one of three approaches. The first is MDX files committed straight to the repo: no database, no editor UI, content ships with a deploy. That works fine until a non-developer needs to edit copy, and every typo becomes a pull request. The second is a generic headless CMS with a React SDK layered on top. Content arrives by API, but the rich text field arrives as a proprietary JSON blob you still have to convert to JSX yourself. Usually it's a tree of typed nodes: a paragraph node, a heading node, an embed node. Each vendor picks its own shape, so the renderer you write for one CMS won't carry over to the next.
Draftbase is the third path. It keeps the git-free editing of a headless CMS. But it stores rich text as plain MDX, so <MDXContent> can render an entry the moment it's fetched. Component reuse works exactly as it does in your repo: drop your own <Callout> or <ProductCard> into an article. Content stays editable outside a deploy.
None of these approaches is always right. Repo-committed MDX still fits a docs site run entirely by engineers, where a pull request is the natural review step. A React CMS earns its place once marketing, support, or content teams need to publish without a developer in the loop, and the result still has to look like the rest of the product rather than an embedded iframe.
The generic-headless-CMS path looks fastest at the start and usually costs the most over time. The first integration is a weekend of work: install an SDK, map a few fields, ship a page. Then the renderer for the rich-text format starts growing, picking up a new case for every block type an editor adds, until it's a small parser your team maintains forever.
Side by side, the three approaches differ on two things. Who edits content, and how much rendering code your team has to maintain.
| Capability | Plain Next.js + Markdown files | Generic headless CMS + React | Draftbase |
|---|---|---|---|
| Content editor for non-devs | None | CMS's own UI, disconnected from repo | Drop-in editor via Draftbase dashboard |
| Type safety | Manual | None. Fields are any | Generated types via SDK codegen |
| Rendering | You write the MDX pipeline yourself | You write a custom rich-text renderer | <MDXContent> ships built in |
| Component reuse in content | Native, since it's your repo | Not supported | Supported. MDX embeds your own React components |
The row that tends to surprise teams is type safety. It looks like a nice-to-have. Until a template gains a field mid-project. The generic-CMS column's any lets that change compile cleanly. It quietly breaks at runtime instead. Generated types turn that into a build-time error. The rendering row carries a similar hidden cost. A custom renderer isn't a one-time build. It's an ongoing job that grows every time an editor asks for a new content block.
How Draftbase implements React-native content
React support here is the storage format, plus two packages that move content into your app. The four pieces below cover the whole path: database entry to typed, rendered component. Your team doesn't assemble that path from smaller parts.
MDX rendering built in
@draftbase/renderer ships <MDXContent>, an async component that evaluates an entry's MDX field and renders it. No separate MDX pipeline to wire up yourself. It accepts a components map, so your own React components render inline.
Typed fields, not any
@draftbase/sdk generates a typed client from your template schema. So entry.fields matches the fields you defined, not any. Add a field, regenerate. The type checker catches every place that needs updating.
Server components fetch directly
The SDK is a plain fetch client. A Server Component can call cms.getEntry() during render. No extra data-fetching layer needed, and no client-side hook or loading state to manage on the page.
Cacheable delivery API
Delivery reads are read-only and API-key gated, separate from the management API. They're safe to wrap in Next.js's fetch cache. Or revalidate on a webhook, so a published entry updates the page in real time, without a redeploy.
85% of new React projects now start with Next.js. That's from the State of React 2025 survey, reported by Strapi. (Source) That default matters for a CMS's SDK. A fetch-based client with no client-only code runs unmodified inside a Server Component. A client built around browser globals doesn't. The same survey found 48% of React developers already use React 19 daily. 41% are still on React 18. (Source) Draftbase's SDK targets both. No version-specific build needed. It's just fetch calls and plain objects. That same SDK works outside React too — see the full framework support list for React Native, Svelte, Vue, and more.
Draft content follows the same path. Entries hold a draft state and a published state, and a Server Component previewing a draft calls cms.entries.get(id), the management-scoped read, rather than the delivery-scoped getEntry. Both return the same field shape, so one <MDXContent> call serves the live page and the preview. No second rendering path to maintain.
Here is the whole path in one file. A Server Component fetches an entry through the typed delivery client and renders its rich text field, with one of your own components made available by name inside the content:
import { notFound } from 'next/navigation';
import { createClient } from '@draftbase/sdk';
import { MDXContent } from '@draftbase/renderer';
import { Callout } from '@/components/Callout';
const draftbase = createClient({ apiKey: process.env.DRAFTBASE_DELIVERY_KEY! });
interface PostFields {
title: string;
body: string;
}
export default async function Post({ params }: { params: { id: string } }) {
const entry = await draftbase.getEntry<PostFields>(params.id);
if (!entry) notFound();
return (
<article>
<h1>{entry.fields.title}</h1>
<MDXContent source={entry.fields.body} components={{ Callout }} />
</article>
);
}No client component, no data-fetching hook, no rich-text walker. getEntry returns null for an entry that isn't published, which is why the notFound() line is there rather than a runtime guard around every field. Generate PostFields from your template with the SDK's codegen and even that interface stops being hand-written.
Schema changes flow through the same generated types. Add a field to a template in the dashboard and it's there the next time you run codegen, typed as optional until it's marked required. No manual mapping file to update on the frontend. And when a component expects a field the schema no longer has, you get a type error at the exact spot that needs fixing, not a blank space on a live page.
React CMS with the Next.js App Router and Server Components
In the App Router, a page is a Server Component by default, so fetching content is a plain await in the component body. The SDK is fetch calls and plain objects with no browser globals, so none of it ships to the browser. A CMS client that assumes a browser environment, or that expects a React hook, forces the opposite: a client component, a loading state, and the client bundle to carry both.
Caching is the next decision, and it belongs to your app rather than to the CMS. Three options, cheapest first. Set export const revalidate on the route segment and Next.js rebuilds the page on a timer. Wrap the fetch in unstable_cache to cache one query independently of the page. Or pass cacheTtlMs when you create the client, which caches read responses inside the SDK itself. For content that has to update the moment it publishes, point a publish webhook at a Route Handler that calls revalidatePath. No redeploy, no polling.
<MDXContent> is an async component because compiling MDX is asynchronous, and an RSC is allowed to await inside its own body. That is the only place this matters: outside RSC — client-side React, a Remix loader — call compileMDX(source) yourself, then render the Content component it returns. Same compilation, one extra line. React Native is the same pattern through its own entry point, @draftbase/renderer/react-native, which also ships default component mappings so it doesn't need one.
'use client' comes in for interactivity inside content, not for the content itself. A <Callout> made of markup stays a Server Component. A tabbed code sample or a pricing slider needs state, so that component carries the directive and gets passed in through the same components map. The page around it stays server-rendered, and only the interactive component reaches the browser.
Best React CMS options compared
Nine products that a React team realistically shortlists. The column that decides the most work is rich text storage: it sets whether you render an entry with one component call or maintain a mapping layer per block type. Prices are the cheapest paid tier at the time of writing, taken from each vendor's own pricing page. Check them before you budget.
| CMS | React integration | Rich text storage | Own components in content | Entry price | Best for |
|---|---|---|---|---|---|
| Contentful | REST/GraphQL API plus a JS SDK | Own JSON document tree | Embedded entries you map to components | Free plan, non-commercial use only (Source) | Enterprise governance and localization at scale |
| Sanity | React-based Studio, JS client | Portable Text JSON | Custom blocks you map to components | Free, then $15 per seat/mo (Source) | The largest plugin and template ecosystem here — more prebuilt pieces than Draftbase has |
| Strapi | Self-hosted Node API, framework-agnostic fetch | Markdown or its own blocks JSON | Custom blocks you map to components | Free self-hosted (MIT); Cloud from $35/mo per project (Source) | Teams that want no license cost and full control of the database — Draftbase is hosted only |
| Storyblok | JS SDK plus a visual editor bridge | Own JSON, component-shaped | Blok types mapped to React components | Free, then $99/mo (Source) | Marketing teams who want visual, in-context page editing |
| Payload | Installs into your own Next.js app | Lexical JSON | Custom blocks you map to components | Free self-hosted (MIT); hosted pricing on request (Source) | Teams who want the CMS inside their Next.js repo — closer to the framework than Draftbase gets |
| Prismic | React SDK built around slices | Own rich text JSON | Slices mapped to React components | Free, then $10/mo per repository (Source) | Page building from a fixed library of sections |
| Hygraph | GraphQL-first, any GraphQL client | Rich text AST JSON | Embeds you map to components | Free, then $199/mo (Source) | Stitching several content sources into one GraphQL schema |
| Cosmic | REST/GraphQL plus a JS SDK | HTML or Markdown | Not without your own parsing | Free, then $49/mo (Source) | Small projects that want a simple object API |
| Draftbase | Typed SDK plus <MDXContent> for RSC | MDX | Registered MDX components with typed props, rendered natively | Free Hobby plan, then $49/mo | React and Next.js teams who want no rich-text renderer to maintain |
Two rows are worth reading against us rather than for us. Payload runs inside your Next.js app, so there is no API boundary to cross at all — if that shape fits your project, it beats any hosted CMS on integration distance. And Strapi self-hosted costs nothing in license fees and keeps the database in your own infrastructure, which Draftbase does not offer.
The one claim we will make outright: every other row stores rich text in a format that needs a mapping layer before React can render it, and Draftbase stores MDX, so a component with typed props written by an editor renders in a Server Component with no walker in between. For a full roundup with feature-by-feature scoring, read the best headless CMS for 2026. This table stays a scannable shortlist.
Common pitfalls building a React CMS integration
The most common mistake: treating a CMS's rich text field as safe HTML. Safe enough to drop straight into the page. Most CMS output isn't sanitized for that. Doing it anyway opens a security hole the moment an editor pastes in untrusted markup. It also blocks component reuse. A raw HTML string can't render a live React component the way JSX can. MDX compiled through a real evaluator, like <MDXContent>, avoids both problems. It parses content as MDX, not trusted HTML. It only renders the components you explicitly pass in.
The second mistake is fetching content on every render with no caching. Say a page has a hero entry, a related-posts list, and an author record. That turns into a waterfall of API calls. One round trip per section. The third mistake is hand-writing TypeScript types for CMS fields. Then letting them drift from the real schema once a field gets added. That mismatch usually shows up as undefined on a live page. Generating types from the schema, the way @draftbase/sdk's codegen does, keeps the two in sync on its own. No one has to remember to update a type file by hand.
Two complete Next.js sites, not starter templates
A storefront and a course platform, each a public repo with a live deployment you can open before cloning. Both run on the App Router: <MDXContent> renders rich text as a Server Component, and generateStaticParams enumerates every page at build time. Nothing is stubbed out.
npx @draftbase/create my-site
Scaffolds any of these, logs you in and mints an API key. CLI source
Storefront
Next.jsA product catalogue with checkout on Stripe Payment Links. No cart server.
- List-valued media
- JSON fields
- Server Components
Start building with a React CMS
Define a template, drop <MDXContent> into a page, and publish an entry in one sitting.
Hobby is free, no card. Startup is $49/mo when you outgrow it. The price is on the pricing page, where prices go.
No migration quarter, no kickoff workshop. Define a template and ship something today.
Frequently asked questions
What is a React CMS?
A React CMS is a content management system that delivers content in a format React can render directly, usually MDX or a component tree, instead of an HTML string or a proprietary rich-text document. Draftbase is a React CMS: rich text fields store MDX, and @draftbase/renderer ships a component that renders it, so there's no translation step between what an editor writes and what your app renders.
Does Draftbase work with Next.js App Router and Server Components?
Yes. The @draftbase/sdk client is a plain fetch-based client with no browser-only APIs, so a Server Component can call cms.getEntry() during render with no extra configuration or client-side data-fetching library. It works the same way inside a Route Handler or a Server Action.
Is the Draftbase React SDK typed?
Yes. @draftbase/sdk generates a typed client from your template schema, so entry.fields matches the fields you defined instead of coming back as any. Adding or renaming a field and regenerating the client surfaces every call site that needs updating as a type error.
Can I use my own React components inside CMS content?
Yes, through MDX. Pass a components map to <MDXContent> and any component name used in an entry (a <Callout> or <ProductCard>, say) renders as that component, the same way it would in an MDX file in your repo. Content editors can drop in interactive UI, not only formatted text.
What is the best headless CMS for React?
The one that returns rich text in a format React renders directly. Contentful, Sanity, and Strapi all ship React SDKs, but each stores rich text in its own JSON shape, so you write and maintain a renderer per vendor. Draftbase stores rich text as MDX and ships <MDXContent>, so rendering an entry is one component call. Check that field first when comparing candidates, then typed clients and draft preview.
Is there a free or open-source React CMS?
Yes. Strapi, Payload, and Directus are open source and self-hostable with no license cost, and each has a React or Next.js integration. You take on hosting, upgrades, and backups in return. Draftbase's Hobby plan is free and hosted, with paid plans from $49/mo. Our free and open-source headless CMS guide compares the self-hosted options in detail.
Do editors manage content in a dashboard or in the repo?
Editors manage content in the Draftbase dashboard, not a developer's local repo. Structured data (JSON-LD) that a page publishes for search engines is generated from the same typed fields an editor filled in, so there's no separate SEO data entry step.
How do I connect a CMS to a React app?
Four steps. Define a template in the CMS for the content you want (a post, a product, a landing page). Mint a read-only delivery API key and put it in a server-side environment variable, never in client code. Install the client — for Draftbase that is npm i @draftbase/sdk @draftbase/renderer — and create it once with createClient({ apiKey }). Then fetch an entry where the page renders: in a Next.js Server Component that is await draftbase.getEntry(id), and the rich text field goes straight into <MDXContent source={entry.fields.body} />. The order matters: template first, key second, fetch last, because the generated types come from the template.
Do I need Next.js to use a React CMS?
No. The delivery API is framework-agnostic — plain HTTP returning JSON — and @draftbase/sdk is a fetch client with no framework dependency, so Vite, Create React App, Remix, React Native, or a plain Node script all work. Next.js gets one convenience: <MDXContent> can await its own compilation as a Server Component. Anywhere else you call compileMDX(source) in your own loader or effect and render the Content component it returns, which is one extra line. Vue, Astro, Svelte, and Angular are covered on the framework support page.
Do I need a separate rich-text renderer for React CMS content?
No. @draftbase/renderer ships <MDXContent>, which evaluates an entry's MDX field and renders it as React elements. There is no proprietary JSON format to write a custom walker for, and no separate markdown pipeline to configure in your app.