Tutorials

Headless CMS for React: How to Choose and Get Started

Jay Callicott··9 min read

Headless CMS for React: How to Choose and Get Started

React applications need content from somewhere. You can hardcode it, pull it from a local JSON file, or wire up a headless CMS that gives you a structured API, an editing interface, and a content model that scales.

Most production React apps outgrow hardcoded content quickly. Marketing copy changes, blog posts accumulate, product descriptions get translated. A headless CMS solves this by separating your content from your frontend code — you model the content once, then fetch it from any React app (or native app, or AI agent) via an API.

This guide covers how to pick a headless CMS for React, what the main options offer, and how to actually wire them up in a React component.

Why React Apps Need a Headless CMS

A headless CMS gives you three things that a JSON file or database table does not:

  1. A content editing UI. Non-developers can create and update content without pushing code. This is table stakes for any team with editors, marketers, or product managers who own content.
  2. Structured content modeling. You define content types (articles, products, landing pages) with typed fields (text, images, references). The CMS enforces the schema, so your frontend always gets predictable data shapes.
  3. An API for delivery. REST or GraphQL endpoints serve published content to your React app. Most platforms also provide preview or draft APIs so editors can see unpublished changes in context.

The "headless" part means the CMS has no opinion about your frontend. It delivers content via API; your React app renders it however you want. This is the fundamental difference from a traditional CMS like WordPress, where the backend and frontend are coupled.

The Main Options

There are dozens of headless CMS platforms. These five come up most often in React projects, each with different trade-offs.

Quick Comparison

Platform API Type React SDK Free Tier Paid From
Contentful REST + GraphQL Yes (official) 10K records, 100K API calls $300/mo
Sanity GROQ + GraphQL Yes (next-sanity) 10K docs, 250K API calls $15/seat/mo
Strapi REST + GraphQL Community Unlimited (self-host) $18/mo (cloud)
Storyblok REST (GraphQL on premium) Yes (@storyblok/react) 20K stories, 100K API calls $99/mo
Decoupled.io JSON:API + GraphQL Yes (Typed Client) Currently in beta Competitive pricing planned

Contentful

Contentful is the most widely adopted headless CMS in the React ecosystem. Its JavaScript SDK (contentful) works in any React app, returns typed responses when paired with their CLI-generated types, and has years of community content behind it.

Strengths: Polished content modeling, large ecosystem, well-documented REST and GraphQL APIs.

Trade-offs: The free tier dropped to 100K API calls in 2025. The next tier jumps to $300/month with no mid-range option. Rich text rendering requires a separate package (@contentful/rich-text-react-renderer), which adds a layer of complexity.

Sanity

Sanity stands out for real-time collaboration and its open-source editing studio. GROQ is a powerful query language, and the next-sanity package is one of the most polished CMS integrations for Next.js specifically.

Strengths: Real-time preview, customizable studio, strong developer community.

Trade-offs: GROQ is proprietary — your query knowledge doesn't transfer to other platforms. Per-seat pricing adds up for larger teams. The backend (Content Lake) is fully proprietary; you cannot self-host it.

Strapi

Strapi is the most popular open-source headless CMS. Self-host it for free, customize it with plugins, and own your data entirely. The REST and GraphQL APIs work with any React client.

Strengths: Open-source (MIT), full data ownership, straightforward content modeling.

Trade-offs: Self-hosting means you handle deployment, scaling, backups, and security patches. No official React SDK — most teams write their own fetching layer. The cloud offering starts cheap but has tight API call limits.

Storyblok

Storyblok's standout feature is its visual editor. Editors click directly on components in the rendered page and edit content in a side panel. The @storyblok/react package bridges the visual editor to your React components.

Strengths: Best visual editing experience for non-technical editors. Good React SDK with editor bridging.

Trade-offs: The component-based content model encodes layout into your content, which creates problems when delivering to non-web targets. GraphQL is locked behind premium tiers. TypeScript types must be defined manually.

Decoupled.io

Decoupled.io is a managed headless CMS built on Drupal. It exposes content through both JSON:API and GraphQL endpoints, and provides a Typed Client that generates a fully typed TypeScript API client from your content schema.

Strengths: Deep content modeling inherited from Drupal (entity references, content moderation, multilingual support). Open-source foundation (GPLv2). The typed client provides IDE autocomplete without a separate code generation step.

Trade-offs: JSON:API's query syntax is less familiar to most React developers than GraphQL or REST. The platform is newer than established players like Contentful or Sanity, so the community ecosystem is still growing.

Fetching CMS Content in a React Component

Regardless of which CMS you choose, the fetching pattern in React is similar. Here is a generic example using fetch and a REST-style API — the same pattern works with Contentful, Strapi, Storyblok, or any CMS that exposes a REST endpoint.

Client-Side Fetching (React with useEffect)

import { useState, useEffect } from 'react'

interface Article {
  id: string
  title: string
  slug: string
  body: string
  publishedAt: string
}

function ArticleList() {
  const [articles, setArticles] = useState<Article[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    async function fetchArticles() {
      const res = await fetch('https://your-cms.example.com/api/articles', {
        headers: {
          Authorization: `Bearer ${process.env.REACT_APP_CMS_TOKEN}`,
        },
      })
      const data = await res.json()
      setArticles(data.items)
      setLoading(false)
    }
    fetchArticles()
  }, [])

  if (loading) return <p>Loading...</p>

  return (
    <ul>
      {articles.map((article) => (
        <li key={article.id}>
          <a href={`/articles/${article.slug}`}>{article.title}</a>
        </li>
      ))}
    </ul>
  )
}

This works, but client-side fetching has downsides: the user sees a loading state, search engines may not index the content reliably, and you expose your API token if you are not careful. For production React apps, server-side fetching is almost always better.

Next.js App Router Integration

If you are using Next.js (which most production React projects are), the App Router's server components let you fetch CMS data on the server with zero client-side JavaScript for the content itself.

// app/articles/page.tsx

interface Article {
  id: string
  title: string
  slug: string
  excerpt: string
}

async function getArticles(): Promise<Article[]> {
  const res = await fetch('https://your-cms.example.com/api/articles', {
    headers: {
      Authorization: `Bearer ${process.env.CMS_API_TOKEN}`,
    },
    next: { revalidate: 60 }, // ISR: revalidate every 60 seconds
  })
  return res.json().then((data) => data.items)
}

export default async function ArticlesPage() {
  const articles = await getArticles()

  return (
    <main>
      <h1>Articles</h1>
      {articles.map((article) => (
        <article key={article.id}>
          <h2>
            <a href={`/articles/${article.slug}`}>{article.title}</a>
          </h2>
          <p>{article.excerpt}</p>
        </article>
      ))}
    </main>
  )
}

Key details in this pattern:

  • Server component by default. No 'use client' directive, so this runs entirely on the server. The CMS API token stays on the server and never reaches the browser.
  • ISR with next: { revalidate } tells Next.js to cache the page and refresh it every 60 seconds. For on-demand revalidation, pair this with a CMS webhook that calls revalidateTag() or revalidatePath().
  • No loading state. The HTML is rendered with content on the server, so users and search engines see complete content on first load.

GraphQL vs REST for React

Both work. The choice usually comes down to your CMS and your team's preferences.

REST (including JSON:API) is simpler to start with. You call an endpoint, get back JSON. Filtering and pagination use query parameters. Most CMS platforms default to REST. The downside is over-fetching — you get all fields on an entity whether you need them or not.

GraphQL lets you request exactly the fields you need. This reduces payload size and makes it explicit what data each component depends on. The trade-off is more tooling: you need a GraphQL client, possibly a code generator for types, and the queries themselves are another layer to maintain.

For React projects, here is a practical guideline:

  • If your CMS provides a typed SDK, use it. Contentful's SDK uses REST under the hood. Sanity uses GROQ. Decoupled.io's Typed Client uses JSON:API. The SDK abstracts the transport, so the choice between REST and GraphQL matters less.
  • If you are writing queries by hand, GraphQL is usually worth the setup cost for anything beyond simple pages. Being able to request exactly what a component needs — and get TypeScript types for that query — reduces bugs and improves performance.
  • If you need to combine data from multiple content types in a single request, GraphQL has a clear advantage. REST typically requires multiple requests or relies on the CMS's "include" or "populate" mechanism.

Decoupled.io supports both JSON:API and GraphQL, so you can use whichever fits your project.

Typed Clients and Type Safety

Type safety matters in React projects. Without it, you are guessing at field names, nullable values, and relationship shapes — and finding out you guessed wrong at runtime.

The approaches vary by CMS:

  • Contentful: Run their CLI to generate TypeScript interfaces from your content model. Import the types into your fetching code. The types and schema stay in sync as long as you re-run the generator after model changes.
  • Sanity: Use sanity-typegen to derive types from your GROQ queries. Types are scoped to what each query returns, not the full schema.
  • Strapi: Use GraphQL Code Generator or manually define interfaces. No official type generation tool.
  • Decoupled.io: The Typed Client generates a fully typed client from the CMS schema with a single CLI command. Types cover all content types, fields, and relationships. Your IDE knows exactly what client.getEntries('NodeArticle') returns — no manual type definitions, no separate generation step during development.

The practical difference is how much friction the type system adds to your workflow. A separate CLI step that generates types is fine for small teams but becomes a pain point when your content model changes frequently. A client that derives types automatically from the schema eliminates that step.

Real-Time Preview

Editors want to see how content looks before they publish. The headless CMS approach makes this harder than traditional CMS tools, because the rendering happens in your React app, not in the CMS itself.

Most platforms solve this with a preview API or draft mode:

  • Contentful provides a separate Content Preview API that returns unpublished entries. You configure your React app to use the preview API when a special cookie or URL parameter is present.
  • Sanity offers real-time preview — content updates appear instantly in the frontend without a page refresh. This is the gold standard for editor experience but requires the next-sanity package.
  • Storyblok takes a visual approach: the editor sees the rendered page inside Storyblok's interface, with clickable regions that open the content editor.
  • Decoupled.io supports draft-mode previews via its API, letting editors see unpublished content rendered in your React or Next.js frontend.

In Next.js, preview mode (now called Draft Mode in the App Router) is the standard mechanism. Your CMS triggers a route that sets a draft cookie, and your data fetching code checks for that cookie to decide whether to fetch published or draft content.

How to Choose

There is no single "best headless CMS for React." The right choice depends on your team and project. Here are honest decision criteria:

Choose Contentful if you want the most mature ecosystem and your organization has budget. It is the safe choice.

Choose Sanity if real-time collaboration and developer customization are priorities. Be prepared for GROQ's learning curve and per-seat pricing.

Choose Strapi if you want open-source and full control, and your team can handle self-hosting.

Choose Storyblok if your editors need visual page building and your content is primarily web-targeted.

Choose Decoupled.io if you need deep content modeling (multilingual, complex entity relationships, content moderation), want an open-source foundation, and value a typed developer experience. It is a particularly good fit if your team already knows Drupal, or if you need to deliver content to multiple frontends from a single source.

For a broader comparison across more criteria, see our headless CMS comparison. If you are specifically evaluating CMS platforms for a Next.js project, the best CMS for Next.js guide goes deeper on framework-specific integration details.

Getting Started

If you want to try Decoupled.io with your React or Next.js project:

  1. Follow the Getting Started guide to set up a Decoupled.io space and configure your content model.
  2. Install the Typed Client to get a fully typed API client for your content.
  3. Use the data fetching patterns from this article to wire your React components to the CMS.

For other platforms, the pattern is the same: set up the CMS, configure your content model, install the SDK (or use fetch), and start building. The code examples in this article work with any CMS that exposes a REST API.