> ## Documentation Index
> Fetch the complete documentation index at: https://buttercms.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# React

> Build content-driven React applications with ButterCMS. Covers setup, fetching pages, collections, components, and blog content.

## Overview

This integration guide shows you how to how to update your existing project to:

1. install the ButterCMS package
2. instantiate ButterCMS
3. create components to fetch and display each of the three ButterCMS content types: [Pages](../../core-concepts/content-types/pages), [Collections](../../core-concepts/content-types/collections), and [Blog Posts](../../core-concepts/content-types/blog-engine).

<Tip>
  In order for the snippets to work, you'll need to [setup your dashboard content schemas inside of ButterCMS](../buttercms-setup) first.
</Tip>

### Starter project

Or, you can jump directly to the starter project below, which will allow you to clone, install, run, and deploy a fully working
starter project that's integrated with content already inside of your ButterCMS account.

<Card title="React Starter Project" icon="rocket" href="../starter-projects/react">
  Hit the ground running with a pre-configured React + ButterCMS setup.
</Card>

## Installation

<Tabs>
  <Tab title="npm">
    ```bash theme={null}
    npm install buttercms
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn add buttercms
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash theme={null}
    pnpm add buttercms
    ```
  </Tab>
</Tabs>

Add your API token to `.env`:

```bash theme={null}
VITE_BUTTER_CMS_API_KEY=your_api_token
```

## Initialize the client

Create a reusable client instance:

```typescript theme={null}
// src/lib/buttercms.ts
import Butter from 'buttercms';

const butter = Butter(import.meta.env.VITE_BUTTER_CMS_API_KEY);

export default butter;
```

<Info>
  For complete SDK documentation including all available methods and configuration options, see the [JavaScript SDK Reference](../sdks/javascript-sdk).
</Info>

## Pages

<Tabs>
  <Tab title="React Router">
    ```tsx theme={null}
    // src/pages/LandingPage.tsx
    import { useEffect, useState } from 'react';
    import { useParams } from 'react-router-dom';
    import butter from '../lib/buttercms';

    interface PageFields {
      headline: string;
      subheadline: string;
      hero_image: string;
      body: string;
    }

    export default function LandingPage() {
      const { slug } = useParams();
      const [page, setPage] = useState<{ fields: PageFields } | null>(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState<string | null>(null);

      useEffect(() => {
        if (!slug) {
          setError('Page not found');
          setLoading(false);
          return;
        }
        async function fetchPage(pageSlug: string) {
          try {
            const response = await butter.page.retrieve<PageFields>('landing_page', pageSlug);
            setPage(response.data?.data ?? null);
          } catch (err) {
            setError('Page not found');
          } finally {
            setLoading(false);
          }
        }
        fetchPage(slug);
      }, [slug]);

      if (loading) return <div>Loading...</div>;
      if (error || !page) return <div>{error || 'Page not found'}</div>;

      return (
        <main>
          <h1>{page.fields.headline}</h1>
          <p>{page.fields.subheadline}</p>
          {page.fields.hero_image && (
            <img src={page.fields.hero_image} alt={page.fields.headline} />
          )}
          <div dangerouslySetInnerHTML={{ __html: page.fields.body ?? '' }} />
        </main>
      );
    }
    ```

    Router setup:

    ```tsx theme={null}
    // src/App.tsx
    import { BrowserRouter, Routes, Route } from 'react-router-dom';
    import LandingPage from './pages/LandingPage';

    export default function App() {
      return (
        <BrowserRouter>
          <Routes>
            <Route path="/:slug" element={<LandingPage />} />
          </Routes>
        </BrowserRouter>
      );
    }
    ```
  </Tab>

  <Tab title="Custom Hook">
    ```tsx theme={null}
    // src/hooks/usePage.ts
    import { useEffect, useState } from 'react';
    import butter from '../lib/buttercms';

    interface Page<T> {
      fields: T;
    }

    interface UsePageResult<T> {
      page: Page<T> | null;
      loading: boolean;
      error: string | null;
    }

    export function usePage<T extends object>(pageType: string, slug: string | undefined): UsePageResult<T> {
      const [page, setPage] = useState<Page<T> | null>(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState<string | null>(null);

      useEffect(() => {
        if (!slug) {
          setError('Page not found');
          setLoading(false);
          return;
        }
        async function fetchPage(pageSlug: string) {
          try {
            const response = await butter.page.retrieve<T>(pageType, pageSlug);
            setPage(response.data?.data ?? null);
          } catch (err) {
            setError('Page not found');
          } finally {
            setLoading(false);
          }
        }
        fetchPage(slug);
      }, [pageType, slug]);

      return { page, loading, error };
    }

    // Usage in component
    // src/pages/LandingPage.tsx
    import { useParams } from 'react-router-dom';
    import { usePage } from '../hooks/usePage';

    interface PageFields {
      headline: string;
      body: string;
    }

    export default function LandingPage() {
      const { slug } = useParams();
      const { page, loading, error } = usePage<PageFields>('landing_page', slug);

      if (loading) return <div>Loading...</div>;
      if (error || !page) return <div>{error}</div>;

      return (
        <main>
          <h1>{page.fields.headline}</h1>
          <div dangerouslySetInnerHTML={{ __html: page.fields.body ?? '' }} />
        </main>
      );
    }
    ```
  </Tab>
</Tabs>

## Collections

```tsx theme={null}
// src/pages/BrandsPage.tsx
import { useEffect, useState } from 'react';
import butter from '../lib/buttercms';

interface Brand {
  name: string;
  logo: string;
  description: string;
}

export default function BrandsPage() {
  const [brands, setBrands] = useState<Brand[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchBrands() {
      try {
        const response = await butter.content.retrieve(['brands']);
        if (!response.data?.data) {
          throw new Error('Failed to load brands from ButterCMS');
        }
        const data = response.data.data as { brands: Brand[] };
        setBrands(data.brands);
      } finally {
        setLoading(false);
      }
    }
    fetchBrands();
  }, []);

  if (loading) return <div>Loading...</div>;

  return (
    <main>
      <h1>Our Brands</h1>
      <ul>
        {brands.map((brand, index) => (
          <li key={index}>
            <img src={brand.logo} alt={brand.name} />
            <h2>{brand.name}</h2>
            <div dangerouslySetInnerHTML={{ __html: brand.description ?? '' }} />
          </li>
        ))}
      </ul>
    </main>
  );
}
```

## Dynamic components

### Component Renderer

```tsx theme={null}
// src/components/ComponentRenderer.tsx
import Hero from './Hero';
import Features from './Features';
import Testimonials from './Testimonials';
import CTA from './CTA';

export type PageComponent =
  | { type: 'hero'; fields: React.ComponentProps<typeof Hero> }
  | { type: 'features'; fields: React.ComponentProps<typeof Features> }
  | { type: 'testimonials'; fields: React.ComponentProps<typeof Testimonials> }
  | { type: 'cta'; fields: React.ComponentProps<typeof CTA> };

export default function ComponentRenderer({ components }: { components: PageComponent[] }) {
  return (
    <>
      {components.map((component, index) => {
        switch (component.type) {
          case 'hero':
            return <Hero key={index} {...component.fields} />;
          case 'features':
            return <Features key={index} {...component.fields} />;
          case 'testimonials':
            return <Testimonials key={index} {...component.fields} />;
          case 'cta':
            return <CTA key={index} {...component.fields} />;
          default:
            return null;
        }
      })}
    </>
  );
}
```

### Example Component

```tsx theme={null}
// src/components/Hero.tsx
interface HeroProps {
  headline: string;
  subheadline: string;
  image: string;
  button_label: string;
  button_url: string;
}

export default function Hero({ headline, subheadline, image, button_label, button_url }: HeroProps) {
  return (
    <section className="hero">
      <h1>{headline}</h1>
      <p>{subheadline}</p>
      {button_label && <a href={button_url}>{button_label}</a>}
      {image && <img src={image} alt={headline} />}
    </section>
  );
}
```

### Using in Pages

<Info>
  This uses a distinct page type (`component_page`) whose `body` is a **Component Picker** (Page Builder) field — an array of components — separate from the WYSIWYG `landing_page` in the Pages section (whose `body` is a string). A page's `body` is one field type or the other, so the Page Builder example needs its own page type.
</Info>

```tsx theme={null}
// src/pages/LandingPage.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import butter from '../lib/buttercms';
import ComponentRenderer, { type PageComponent } from '../components/ComponentRenderer';

interface PageFields {
  body: PageComponent[];
}

export default function LandingPage() {
  const { slug } = useParams();
  const [page, setPage] = useState<{ fields: PageFields } | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (!slug) {
      setLoading(false);
      return;
    }
    async function fetchPage(pageSlug: string) {
      try {
        const response = await butter.page.retrieve<PageFields>('component_page', pageSlug);
        setPage(response.data?.data ?? null);
      } finally {
        setLoading(false);
      }
    }
    fetchPage(slug);
  }, [slug]);

  if (loading) return <div>Loading...</div>;
  if (!page) return <div>Page not found</div>;

  return (
    <main>
      <ComponentRenderer components={page.fields.body} />
    </main>
  );
}
```

## Blog

<Tabs>
  <Tab title="Blog Post List">
    ```tsx theme={null}
    // src/pages/BlogList.tsx
    import { useEffect, useState } from 'react';
    import { Link, useSearchParams } from 'react-router-dom';
    import butter from '../lib/buttercms';

    interface Post {
      slug: string;
      title: string;
      summary: string;
      author: { first_name: string; last_name: string };
    }

    interface Meta {
      next_page: number | null;
      previous_page: number | null;
    }

    export default function BlogList() {
      const [posts, setPosts] = useState<Post[]>([]);
      const [meta, setMeta] = useState<Meta | null>(null);
      const [loading, setLoading] = useState(true);
      const [searchParams] = useSearchParams();
      const page = parseInt(searchParams.get('page') || '1');

      useEffect(() => {
        async function fetchPosts() {
          setLoading(true);
          try {
            const response = await butter.post.list({ page, page_size: 10 });
            if (!response.data?.data) {
              throw new Error('Failed to load posts from ButterCMS');
            }
            setPosts(response.data.data);
            setMeta(response.data.meta);
          } finally {
            setLoading(false);
          }
        }
        fetchPosts();
      }, [page]);

      if (loading) return <div>Loading...</div>;

      return (
        <main>
          <h1>Blog</h1>
          <ul>
            {posts.map((post) => (
              <li key={post.slug}>
                <h2><Link to={`/blog/${post.slug}`}>{post.title}</Link></h2>
                <p dangerouslySetInnerHTML={{ __html: post.summary ?? '' }} />
                <span>By {post.author.first_name} {post.author.last_name}</span>
              </li>
            ))}
          </ul>
          {meta?.next_page && (
            <Link to={`/blog?page=${meta.next_page}`}>Next Page</Link>
          )}
        </main>
      );
    }
    ```
  </Tab>

  <Tab title="Single Blog Post">
    ```tsx theme={null}
    // src/pages/BlogPost.tsx
    import { useEffect, useState } from 'react';
    import { useParams, Link } from 'react-router-dom';
    import butter from '../lib/buttercms';

    interface Post {
      title: string;
      body: string | null;
      published: string | null;
      featured_image: string | null;
      author: { first_name: string; last_name: string };
    }

    export default function BlogPost() {
      const { slug } = useParams();
      const [post, setPost] = useState<Post | null>(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState<string | null>(null);

      useEffect(() => {
        if (!slug) {
          setError('Post not found');
          setLoading(false);
          return;
        }
        async function fetchPost(postSlug: string) {
          try {
            const response = await butter.post.retrieve(postSlug);
            setPost(response.data?.data ?? null);
          } catch {
            setError('Post not found');
          } finally {
            setLoading(false);
          }
        }
        fetchPost(slug);
      }, [slug]);

      if (loading) return <div>Loading...</div>;
      if (error || !post) return <div>{error || 'Post not found'}</div>;

      return (
        <article>
          <h1>{post.title}</h1>
          <p>
            By {post.author.first_name} {post.author.last_name}
            {post.published && (
              <> on {new Date(post.published).toLocaleDateString()}</>
            )}
          </p>
          {post.featured_image && <img src={post.featured_image} alt={post.title} />}
          <div dangerouslySetInnerHTML={{ __html: post.body ?? '' }} />
          <Link to="/blog">Back to Posts</Link>
        </article>
      );
    }
    ```
  </Tab>
</Tabs>

## SEO with React Helmet

```tsx theme={null}
// src/pages/LandingPage.tsx
import { Helmet } from 'react-helmet-async';
import { usePage } from '../hooks/usePage';

interface Seo {
  title?: string;
  description?: string;
  og_title?: string;
  og_description?: string;
  og_image?: string;
}

interface PageFields {
  headline: string;
  body: string | null;
  seo?: Seo;
}

export default function LandingPage({ slug }: { slug: string }) {
  const { page } = usePage<PageFields>('landing_page', slug);

  if (!page) return null;

  const seo = page.fields.seo ?? {};

  return (
    <>
      <Helmet>
        <title>{seo.title || page.fields.headline}</title>
        <meta name="description" content={seo.description} />
        <meta property="og:title" content={seo.og_title || seo.title} />
        <meta property="og:description" content={seo.og_description || seo.description} />
        {seo.og_image && <meta property="og:image" content={seo.og_image} />}
      </Helmet>
      <main>
        <h1>{page.fields.headline}</h1>
        <div dangerouslySetInnerHTML={{ __html: page.fields.body ?? '' }} />
      </main>
    </>
  );
}
```

## Resources

<CardGroup cols={2}>
  <Card title="React Starter" icon="rocket" href="../starter-projects/react">
    Pre-configured starter project
  </Card>

  <Card title="JavaScript SDK" icon="js" href="../sdks/javascript-sdk">
    Complete SDK reference
  </Card>

  <Card title="GitHub Repository" icon="github" href="https://github.com/ButterCMS/react-starter-buttercms">
    View source code
  </Card>

  <Card title="Content API" icon="database" href="../../api-reference/pages/get-multiple-pages">
    REST API documentation
  </Card>
</CardGroup>
