DEV Community

Paradane
Paradane

Posted on

Comparing Headless CMS Options for React: A Practical Tutorial

Comparing Headless CMS Options for React: A Practical Tutorial

If you've built a React website, you've likely faced the challenge of managing content—blog posts, product descriptions, testimonials—directly within your components. This often leads to tightly coupled code where every content change requires a developer to edit JSX, re-deploy, and hope nothing breaks. The result? A brittle frontend that's hard to maintain and scale. A headless CMS solves this by decoupling content management from presentation. It provides a backend to author and store content, delivering it to your React app via APIs. This means designers manage content in a dashboard, while React fetches it on demand. In this tutorial, we'll compare three leading headless CMS options: Contentful (cloud-based, GraphQL), Strapi (self-hosted, REST/GraphQL), and Sanity (cloud with real-time collaboration). We'll walk through integrating each with React, examining developer experience, setup complexity, and key trade-offs. By the end, you'll have a practical framework for choosing the best CMS for your next React project and hands-on snippets to get started immediately.

Understanding Headless CMS and Its Fit with React's Component Model

A traditional CMS (like WordPress or Drupal) tightly couples content creation with presentation—the content lives inside templates and is rendered server-side. This makes it difficult to reuse content across different platforms or modern frontends like React. A headless CMS takes the opposite approach: it focuses solely on content storage and delivery, exposing structured data via APIs (REST or GraphQL) with no built-in presentation layer. This decoupling allows developers to consume content from any frontend framework.

React’s component-based architecture is a natural fit for this model. Instead of embedding content in a monolithic template, you build reusable components that fetch and render data from a headless CMS. This is often called the fetch-and-render pattern: components call API endpoints on mount, receive JSON content, and display it. For example, a HeroSection component can fetch its title, subtitle, and background image from a CMS entry. The same component can be reused across multiple pages, each time fetching different content from the CMS.

This pattern yields several benefits. First, component reusability improves dramatically—you build a component once and populate it with dynamic content. Second, content updates become API-driven: editors change content in the CMS admin panel, and the React app retrieves the latest version on next render (or through caching strategies). Third, developer experience improves because frontend and content teams can work in parallel. Designers iterate on components while editors fill in content, without stepping on each other’s toes. The headless approach also integrates well with modern React patterns like server-side rendering (Next.js), static generation (Gatsby), or client-side fetching, giving you flexibility to choose the best delivery method for your project.

Key Criteria for Choosing a Headless CMS for React

When evaluating headless CMS options for a React project, the decision framework should center on three axes: hosting model, content modeling flexibility, and API capabilities. Each CMS in this comparison—Contentful, Strapi, and Sanity—takes a different approach.

Hosting: Cloud vs. Self-Hosted
Contentful and Sanity are fully cloud-hosted platforms, meaning they manage infrastructure, uptime, and scaling for you. This is ideal for teams that want zero DevOps overhead. Strapi, by contrast, is self-hosted (though a cloud version now exists). If your team requires full data control or operates in a regulated industry, Strapi’s self-hosted model gives you that autonomy, but it also means you’re responsible for server maintenance, backups, and scaling.

Content Modeling Flexibility
A CMS’s content modeling capability determines how easily you can structure your data. Contentful offers an intuitive UI for defining content types and relationships, but its free tier limits you to two user seats and 10,000 records. Strapi provides a powerful admin panel where you can define dynamic zones and components, but custom fields require a plugin ecosystem. Sanity stands out with its schema-as-code approach—you define content structures in JavaScript, which aligns naturally with React’s component architecture and enables version-controlled content logic.

API Options: REST vs. GraphQL
Contentful offers both REST and GraphQL endpoints, giving you flexibility to query exactly the data you need. Strapi recently added a GraphQL plugin, but its REST API is more mature. Sanity exclusively uses GraphQL (via GROQ queries), which can be a steep learning curve but rewards you with precise, efficient data fetching. For React apps that rely on Apollo Client or Relay, Sanity’s GraphQL-first design is a strong match; for simpler apps, Contentful’s REST API might be faster to implement.

SDK Support and Developer Experience
All three CMSs provide official JavaScript/React SDKs that handle authentication, caching, and pagination. Contentful’s SDK is lightweight and well-documented, but its learning curve lies in mastering its delivery API. Strapi’s SDK is straightforward, but its documentation can be inconsistent across versions. Sanity offers a mature client (@sanity/client) and GROQ query builder, plus the @sanity/image-url utility for responsive images. Sanity’s developer experience is often praised for its real-time collaboration and hot-reload preview, which speeds up content iteration.

Pricing Considerations
For startups or small teams, Contentful’s free tier is generous but limits content records and team seats. Strapi’s self-hosted model is free (just pay for your server), making it budget-friendly for early-stage projects. Sanity’s free tier includes generous usage limits (up to 3 API users, 100,000 documents) and scales transparently. Enterprises with high traffic and complex workflows should evaluate Contentful’s enterprise SLAs and Sanity’s custom plans. Strapi’s enterprise offering focuses on support and compliance but adds cost for managed hosting.

Scalability
Contentful and Sanity are built for high-traffic, multi-tenant architectures; their CDN caching ensures fast content delivery globally. Strapi’s scalability depends on your own infrastructure, but it can be horizontally scaled with database replication and load balancers. If you expect rapid growth, cloud-native options like Contentful and Sanity reduce operational risk.

Ultimately, the best CMS for your React project aligns with your team’s hosting preference, content modeling needs, and comfort with API paradigms. Start by listing your must-haves—then test-drive the CMS that fits the most criteria.

Integrating Contentful with a React Application

Contentful is a popular cloud-hosted headless CMS that offers a robust content modeling system and a developer-friendly API. This section will walk you through setting up a Contentful space, defining content models, installing the necessary SDKs, and fetching content in a React component. You'll also learn how to manage environment variables, handle rich text, and consider deployment best practices.

Step 1: Create a Contentful Space and Define Content Models

First, sign up for a free Contentful account and create a new space. Contentful organizes content into "content types" which act as schemas for your data. For this tutorial, create a content type called "Blog Post" with fields for title (short text), slug (short text), body (rich text), and featured image (media).

  • In the Contentful web app, go to Content Model > Add Content Type and name it "Blog Post".
  • Add fields accordingly. Make title and slug required, and set slug to be unique.
  • Once saved, create a few sample entries under the Content tab.

Step 2: Install the Contentful SDK and Rich Text Renderer

In your React project, install the Contentful JavaScript SDK and a helper for rendering rich text:

npm install contentful @contentful/rich-text-react-renderer
Enter fullscreen mode Exit fullscreen mode

The SDK will handle authentication and API calls, while the rich text renderer will convert Contentful's rich text JSON into React components.

Step 3: Set Up Environment Variables

To keep your API keys secure, store them in an .env file at the root of your React project. Never commit this file to version control.

REACT_APP_CONTENTFUL_SPACE_ID=your_space_id
REACT_APP_CONTENTFUL_ACCESS_TOKEN=your_content_delivery_api_token
Enter fullscreen mode Exit fullscreen mode

In React, environment variables prefixed with REACT_APP_ are available at runtime. You can access them via process.env.REACT_APP_CONTENTFUL_SPACE_ID.

Step 4: Create a Contentful Client Utility

Create a file called contentfulClient.js to initialize the SDK client:

import { createClient } from 'contentful';

export const client = createClient({
  space: process.env.REACT_APP_CONTENTFUL_SPACE_ID,
  accessToken: process.env.REACT_APP_CONTENTFUL_ACCESS_TOKEN,
});
Enter fullscreen mode Exit fullscreen mode

Step 5: Fetch and Render Content in a React Component

Now, build a component that fetches blog posts and renders them. Use useEffect and useState for data fetching:

import React, { useState, useEffect } from 'react';
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { client } from './contentfulClient';

const BlogPosts = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    client.getEntries({ content_type: 'blogPost' })
      .then((response) => setPosts(response.items))
      .catch(console.error);
  }, []);

  return (
    <div>
      {posts.map((post) => (
        <article key={post.sys.id}>
          <h2>{post.fields.title}</h2>
          <div>
            {documentToReactComponents(post.fields.body)}
          </div>
        </article>
      ))}
    </div>
  );
};

export default BlogPosts;
Enter fullscreen mode Exit fullscreen mode

Notice how client.getEntries accepts a filter object—here we filter by the content type ID (blogPost matches the API name of your content type). The rich text body is rendered using documentToReactComponents, which handles embedded images and other blocks seamlessly.

Step 6: Preview Mode and Deployment Best Practices

Contentful offers a Preview API that shows unpublished content. For a preview environment, create a separate client using the preview token and the preview.contentful.com host. Toggle between preview and delivery clients based on environment variables.

When deploying your React app, ensure the production build uses the delivery API token and the correct space ID. On platforms like Vercel or Netlify, set environment variables via their UI. Always restrict access to the Contentful Delivery API by IP if possible.

Wrapping Up

Integrating Contentful with React is straightforward thanks to the official SDK and rich text renderer. By following these steps, you can set up a maintainable content management workflow that keeps your React components clean and your content team empowered. In the next section, we'll explore Strapi, a self-hosted alternative, and see how it compares.

Integrating Strapi with a React Application

Strapi is an open-source, self-hosted headless CMS that gives you full control over your content and infrastructure. Unlike cloud-based options, Strapi runs on your own server, making it a strong choice for teams that need data sovereignty, custom plugins, or specific compliance requirements. In this section, you'll set up a local Strapi project, define content types, expose public API endpoints, and connect a React frontend to fetch and display content.

Step 1: Initialize a Strapi Project

First, ensure you have Node.js (v18 or later) installed. Create a new Strapi project using the CLI:

npx create-strapi-app@latest my-strapi-project --quickstart
Enter fullscreen mode Exit fullscreen mode

The --quickstart flag automatically sets up SQLite as the database and starts the development server. For production, you'll likely switch to PostgreSQL or MySQL. After the command completes, Strapi will be running at http://localhost:1337/admin. Create an admin account to access the dashboard.

Step 2: Define Content Types via the Admin Panel

Strapi's admin panel allows you to create content types (collections) without writing code. For a blog, you might create a Post content type with fields like title (Text), content (Rich Text), and coverImage (Media). Navigate to Content-Type BuilderCreate new collection type and add your fields. Strapi automatically generates REST and GraphQL API endpoints for each content type.

Step 3: Configure Public API Permissions

By default, new content types are not accessible via the public API. To allow unauthenticated read access, go to SettingsUsers & Permissions pluginRolesPublic. Under Permissions, find your content type (e.g., Post) and check the find and findOne boxes. Save the changes. Now your API is publicly readable.

Step 4: Connect React to Strapi Using Fetch

In your React app, install no additional SDK is required—standard HTTP clients work fine. Create a service file to centralize API calls:

// services/strapi.js
const API_URL = 'http://localhost:1337/api';

export const fetchPosts = async () => {
  const response = await fetch(`${API_URL}/posts?populate=*`);
  if (!response.ok) {
    throw new Error('Failed to fetch posts');
  }
  return response.json();
};
Enter fullscreen mode Exit fullscreen mode

The populate=* parameter tells Strapi to include related media fields. In your component, call this function:

import { useEffect, useState } from 'react';
import { fetchPosts } from './services/strapi';

function PostsList() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetchPosts().then((data) => setPosts(data.data));
  }, []);

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.attributes.title}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Handle Media (Images) from Strapi

Strapi stores uploaded files (images, PDFs) as media objects. When you fetch a post with populate=*, the coverImage field contains an object with a url property. You need to prepend the Strapi base URL to construct the full image URL. For example:

const imageUrl = `http://localhost:1337${post.attributes.coverImage.data.attributes.url}`;
Enter fullscreen mode Exit fullscreen mode

Use this in an <img> tag. In production, store the base URL in an environment variable.

Step 6: Deploying Strapi

Strapi can be deployed to any Node.js hosting environment. Common options include:

  • Cloud VPS (DigitalOcean, AWS EC2): Full control, but requires manual setup of database, SSL, and process management.
  • Platform-as-a-Service (Railway, Heroku): Easier deployment, but may have scaling limitations.
  • Docker: Package Strapi in a container for consistent deployment.

When deploying, switch to a production database (PostgreSQL recommended), set environment variables for security (e.g., JWT secret, database URL), and configure a reverse proxy like Nginx. Strapi provides a comprehensive deployment guide in its documentation.

With Strapi, you benefit from complete ownership of your content layer, making it a flexible choice for React projects that require custom backend logic or need to stay within a specific infrastructure.

Integrating Sanity with a React Application

Sanity takes a developer-first approach by treating content as structured data. Unlike Contentful’s visual editor or Strapi’s admin panel, Sanity Studio is a customizable React application itself, giving you full control over the editing experience. Let’s walk through connecting Sanity to a React frontend.

Step 1: Create a Sanity Project and Define Schemas

First, install the Sanity CLI and create a new project:

npm install -g @sanity/cli
sanity init
Enter fullscreen mode Exit fullscreen mode

During initialization, you’ll choose a project name, dataset (e.g., production), and output folder. Sanity stores content in a cloud-hosted, real-time database. Once your project is ready, navigate into the Studio directory and define a schema. Schemas are plain JavaScript objects. For example, a blog post schema (schemas/post.js):

export default {
  name: 'post',
  title: 'Blog Post',
  type: 'document',
  fields: [
    { name: 'title', type: 'string', title: 'Title' },
    { name: 'slug', type: 'slug', title: 'Slug', options: { source: 'title' } },
    { name: 'body', type: 'blockContent', title: 'Body' },
    { name: 'mainImage', type: 'image', title: 'Main image',
      options: { hotspot: true } }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The hotspot: true option enables Sanity’s image hot‑spotting feature, which lets editors define the focal point of an image. This is especially useful for responsive layouts because you can crop images intelligently without losing important visual context.

Step 2: Install and Configure @sanity/client in Your React App

In your React application, install the official Sanity client and image URL helper:

npm install @sanity/client @sanity/image-url
Enter fullscreen mode Exit fullscreen mode

Create a file sanity.js with your project configuration:

import sanityClient from '@sanity/client'
import imageUrlBuilder from '@sanity/image-url'

export const client = sanityClient({
  projectId: 'your-project-id',
  dataset: 'production',
  apiVersion: '2023-05-03', // use current UTC date
  useCdn: true
})

const builder = imageUrlBuilder(client)
export const urlFor = (source) => builder.image(source)
Enter fullscreen mode Exit fullscreen mode

Replace your-project-id with the actual ID from your Sanity project dashboard. Storing the project ID in an environment variable is recommended for security.

Step 3: Fetch Content with GROQ Queries

GROQ (Graph-Relational Object Queries) is Sanity’s powerful query language. It’s like GraphQL but designed specifically for fetching structured content. Here’s a query to retrieve all blog posts with their main image:

*[_type == "post"] | order(publishedAt desc) {
  title,
  slug,
  body,
  "imageUrl": mainImage.asset->url
}
Enter fullscreen mode Exit fullscreen mode

In a React component, use the client.fetch method:

import { useState, useEffect } from 'react'
import { client } from '../sanity'

export default function BlogPosts() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    client.fetch(`*[_type == "post"] | order(publishedAt desc) {
      title,
      slug,
      body,
      mainImage
    }`)
    .then(data => setPosts(data))
    .catch(console.error)
  }, [])

  return (
    <ul>
      {posts.map(post => (
        <li key={post.slug.current}>
          <h2>{post.title}</h2>
          {post.mainImage && (
            <img src={urlFor(post.mainImage).width(800).url()} alt={post.title} />
          )}
        </li>
      ))}
    </ul>
  )
}
Enter fullscreen mode Exit fullscreen mode

Notice the urlFor helper from @sanity/image-url. It transforms the raw image reference into a URL that can include parameters like width, height, crop, and fit. Combined with hotspot data, this generates responsive images that adapt to any container.

Real‑Time Collaboration and Studio

One of Sanity’s standout features is real‑time collaboration. Multiple editors can work on the same document simultaneously, and changes appear in the frontend instantly when you use the live preview functionality. The Sanity Studio, which runs as a single‑page application, can be hosted on Sanity’s infrastructure or your own. You can customize its structure, input components, and even embed it inside your React app for an integrated editing experience.

By using Sanity with React, you gain a highly flexible content management for React websites that aligns with component‑driven development. The combination of structured schemas, GROQ, and an image pipeline makes it ideal for projects that require precise control over content presentation.

Applying This Guide to Your Next Project

You now have a practical understanding of three headless CMS options—Contentful, Strapi, and Sanity—and how to integrate each with a React application. The key is to choose based on your project’s specific needs: Contentful shines for teams that want a fully managed, enterprise-ready solution; Strapi gives you self-hosted flexibility and full control over your content API; Sanity offers real-time collaboration and structured content with GROQ queries. No single option is universally best—match the CMS to your team’s size, content complexity, and deployment preferences.

Start small. Pick one CMS, prototype a simple feature like a blog or portfolio section, and integrate it iteratively. Use environment variables for API keys, define your content models early, and test how the data flows into your React components. This hands-on approach will reveal which workflow feels most natural for your team.

If your team needs expert guidance on CMS selection or full-stack development, Paradane (https://paradane.com) can help you move from prototype to production efficiently. Remember, the goal is to decouple content management from presentation, giving your React app the flexibility it deserves. Apply the patterns from this tutorial to your next project and see how much cleaner your architecture becomes.

Top comments (0)