Skip to main 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, Collections, and Blog Posts.
In order for the snippets to work, you’ll need to setup your dashboard content schemas inside of ButterCMS first.

Installation

npm install buttercms react-native-render-html
For environment variables, install react-native-config:
npm install react-native-config

Initialize the client

// lib/buttercms.ts
import Butter from 'buttercms';
import Config from 'react-native-config';

const butter = Butter(Config.BUTTERCMS_API_TOKEN);

export default butter;
Add to .env:
BUTTERCMS_API_TOKEN=your_api_token
For complete SDK documentation including all available methods and configuration options, see the JavaScript SDK Reference.

Custom hooks

// hooks/useButter.ts
import { useState, useEffect, useCallback } from 'react';
import butter from '../lib/buttercms';

interface UsePageResult<T> {
  page: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => void;
}

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

  const fetchPage = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await butter.page.retrieve(pageType, slug);
      setPage(response.data.data);
    } catch (err) {
      setError(err as Error);
    } finally {
      setLoading(false);
    }
  }, [pageType, slug]);

  useEffect(() => {
    fetchPage();
  }, [fetchPage]);

  return { page, loading, error, refetch: fetchPage };
}

export function useCollection(keys: string[]) {
  const [data, setData] = useState<Record<string, any> | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    async function fetchCollection() {
      try {
        const response = await butter.content.retrieve(keys);
        setData(response.data.data);
      } catch (err) {
        setError(err as Error);
      } finally {
        setLoading(false);
      }
    }
    fetchCollection();
  }, [keys.join(',')]);

  return { data, loading, error };
}

export function usePosts(page = 1, pageSize = 10) {
  const [posts, setPosts] = useState<any[]>([]);
  const [meta, setMeta] = useState<any>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchPosts() {
      setLoading(true);
      try {
        const response = await butter.post.list({ page, page_size: pageSize });
        setPosts(response.data.data);
        setMeta(response.data.meta);
      } finally {
        setLoading(false);
      }
    }
    fetchPosts();
  }, [page, pageSize]);

  return { posts, meta, loading };
}

export function usePost(slug: string) {
  const [post, setPost] = useState<any>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    async function fetchPost() {
      try {
        const response = await butter.post.retrieve(slug);
        setPost(response.data.data);
      } catch (err) {
        setError(err as Error);
      } finally {
        setLoading(false);
      }
    }
    fetchPost();
  }, [slug]);

  return { post, loading, error };
}

Pages

// screens/LandingPage.tsx
import React from 'react';
import {
  ScrollView,
  Text,
  Image,
  StyleSheet,
  ActivityIndicator,
  View
} from 'react-native';
import { usePage } from '../hooks/useButter';
import RenderHtml from 'react-native-render-html';
import { useWindowDimensions } from 'react-native';

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

interface PageData {
  fields: PageFields;
  slug: string;
}

export default function LandingPage({ route }) {
  const { slug = 'home' } = route.params || {};
  const { page, loading, error } = usePage<PageData>('landing-page', slug);
  const { width } = useWindowDimensions();

  if (loading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#007AFF" />
      </View>
    );
  }

  if (error || !page) {
    return (
      <View style={styles.centered}>
        <Text style={styles.error}>Page not found</Text>
      </View>
    );
  }

  return (
    <ScrollView style={styles.container}>
      <Text style={styles.headline}>{page.fields.headline}</Text>
      <Text style={styles.subheadline}>{page.fields.subheadline}</Text>

      {page.fields.hero_image && (
        <Image
          source={{ uri: page.fields.hero_image }}
          style={styles.heroImage}
          resizeMode="cover"
        />
      )}

      <RenderHtml
        contentWidth={width - 32}
        source={{ html: page.fields.body }}
      />
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  error: { textAlign: 'center', color: '#ff3b30' },
  headline: { fontSize: 28, fontWeight: 'bold', marginBottom: 8 },
  subheadline: { fontSize: 16, color: '#666', marginBottom: 16 },
  heroImage: { width: '100%', height: 200, borderRadius: 8, marginBottom: 16 },
});

Collections

// screens/BrandsScreen.tsx
import React from 'react';
import {
  FlatList,
  Text,
  Image,
  View,
  StyleSheet,
  ActivityIndicator
} from 'react-native';
import { useCollection } from '../hooks/useButter';
import RenderHtml from 'react-native-render-html';
import { useWindowDimensions } from 'react-native';

export default function BrandsScreen() {
  const { data, loading, error } = useCollection(['brands']);
  const { width } = useWindowDimensions();

  if (loading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#007AFF" />
      </View>
    );
  }

  if (error || !data?.brands) {
    return (
      <View style={styles.centered}>
        <Text style={styles.error}>Failed to load brands</Text>
      </View>
    );
  }

  return (
    <FlatList
      data={data.brands}
      keyExtractor={(item, index) => `brand-${index}`}
      contentContainerStyle={styles.container}
      renderItem={({ item }) => (
        <View style={styles.brandCard}>
          {item.logo && (
            <Image source={{ uri: item.logo }} style={styles.logo} />
          )}
          <Text style={styles.brandName}>{item.name}</Text>
          <RenderHtml
            contentWidth={width - 64}
            source={{ html: item.description }}
          />
        </View>
      )}
    />
  );
}

const styles = StyleSheet.create({
  container: { padding: 16 },
  centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  error: { color: '#ff3b30' },
  brandCard: {
    backgroundColor: '#fff',
    padding: 16,
    borderRadius: 12,
    marginBottom: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  logo: { width: 80, height: 80, borderRadius: 8, marginBottom: 12 },
  brandName: { fontSize: 20, fontWeight: '600', marginBottom: 8 },
});

Dynamic components

Component Renderer

// components/ComponentRenderer.tsx
import React from 'react';
import { View } from 'react-native';
import HeroComponent from './HeroComponent';
import FeaturesComponent from './FeaturesComponent';
import CTAComponent from './CTAComponent';

interface Component {
  type: string;
  fields: Record<string, any>;
}

interface Props {
  components: Component[];
}

export default function ComponentRenderer({ components }: Props) {
  return (
    <View>
      {components.map((component, index) => {
        switch (component.type) {
          case 'hero':
            return <HeroComponent key={index} fields={component.fields} />;
          case 'features':
            return <FeaturesComponent key={index} fields={component.fields} />;
          case 'cta':
            return <CTAComponent key={index} fields={component.fields} />;
          default:
            return null;
        }
      })}
    </View>
  );
}

Example Component

// components/HeroComponent.tsx
import React from 'react';
import { View, Text, Image, TouchableOpacity, StyleSheet, Linking } from 'react-native';

interface HeroFields {
  headline: string;
  subheadline: string;
  image?: string;
  button_label?: string;
  button_url?: string;
}

export default function HeroComponent({ fields }: { fields: HeroFields }) {
  const handlePress = () => {
    if (fields.button_url) {
      Linking.openURL(fields.button_url);
    }
  };

  return (
    <View style={styles.hero}>
      <Text style={styles.headline}>{fields.headline}</Text>
      <Text style={styles.subheadline}>{fields.subheadline}</Text>

      {fields.button_label && (
        <TouchableOpacity style={styles.button} onPress={handlePress}>
          <Text style={styles.buttonText}>{fields.button_label}</Text>
        </TouchableOpacity>
      )}

      {fields.image && (
        <Image
          source={{ uri: fields.image }}
          style={styles.image}
          resizeMode="cover"
        />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  hero: { padding: 24, alignItems: 'center' },
  headline: { fontSize: 32, fontWeight: 'bold', textAlign: 'center', marginBottom: 12 },
  subheadline: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 20 },
  button: { backgroundColor: '#007AFF', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8 },
  buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
  image: { width: '100%', height: 200, borderRadius: 12, marginTop: 20 },
});

Using in Pages

// screens/ComponentPage.tsx
import React from 'react';
import { ScrollView, ActivityIndicator, View, Text, StyleSheet } from 'react-native';
import { usePage } from '../hooks/useButter';
import ComponentRenderer from '../components/ComponentRenderer';

interface ComponentPageFields {
  body: Array<{ type: string; fields: Record<string, any> }>;
}

export default function ComponentPage({ route }) {
  const { slug } = route.params;
  const { page, loading, error } = usePage<{ fields: ComponentPageFields }>('landing-page', slug);

  if (loading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#007AFF" />
      </View>
    );
  }

  if (error || !page) {
    return (
      <View style={styles.centered}>
        <Text style={styles.error}>Page not found</Text>
      </View>
    );
  }

  return (
    <ScrollView>
      <ComponentRenderer components={page.fields.body || []} />
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  error: { color: '#ff3b30' },
});

Blog

// screens/BlogList.tsx
import React from 'react';
import {
  FlatList,
  Text,
  TouchableOpacity,
  View,
  Image,
  StyleSheet,
  ActivityIndicator,
} from 'react-native';
import { usePosts } from '../hooks/useButter';

export default function BlogList({ navigation }) {
  const { posts, meta, loading } = usePosts();

  if (loading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#007AFF" />
      </View>
    );
  }

  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.slug}
      contentContainerStyle={styles.container}
      renderItem={({ item }) => (
        <TouchableOpacity
          style={styles.postCard}
          onPress={() => navigation.navigate('BlogPost', { slug: item.slug })}
        >
          {item.featured_image && (
            <Image
              source={{ uri: item.featured_image }}
              style={styles.thumbnail}
            />
          )}
          <View style={styles.postContent}>
            <Text style={styles.postTitle}>{item.title}</Text>
            <Text style={styles.postAuthor}>
              By {item.author.first_name} {item.author.last_name}
            </Text>
            <Text style={styles.postSummary} numberOfLines={2}>
              {item.summary}
            </Text>
          </View>
        </TouchableOpacity>
      )}
    />
  );
}

const styles = StyleSheet.create({
  container: { padding: 16 },
  centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  postCard: {
    backgroundColor: '#fff',
    borderRadius: 12,
    marginBottom: 16,
    overflow: 'hidden',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  thumbnail: { width: '100%', height: 150 },
  postContent: { padding: 16 },
  postTitle: { fontSize: 18, fontWeight: '600', marginBottom: 4 },
  postAuthor: { fontSize: 14, color: '#666', marginBottom: 8 },
  postSummary: { fontSize: 14, color: '#333' },
});
// App.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import LandingPage from './screens/LandingPage';
import BrandsScreen from './screens/BrandsScreen';
import ComponentPage from './screens/ComponentPage';
import BlogList from './screens/BlogList';
import BlogPost from './screens/BlogPost';

const Stack = createNativeStackNavigator();
const queryClient = new QueryClient();

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen name="Home" component={LandingPage} />
          <Stack.Screen name="Brands" component={BrandsScreen} />
          <Stack.Screen name="Landing" component={ComponentPage} />
          <Stack.Screen name="Blog" component={BlogList} />
          <Stack.Screen name="BlogPost" component={BlogPost} />
        </Stack.Navigator>
      </NavigationContainer>
    </QueryClientProvider>
  );
}

Caching

// With React Query, caching is built-in
// Configure cache time in the QueryClient

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      cacheTime: 1000 * 60 * 30, // 30 minutes
    },
  },
});

// Manual cache invalidation
import { useQueryClient } from '@tanstack/react-query';

function useInvalidateCache() {
  const queryClient = useQueryClient();

  const invalidatePages = () => queryClient.invalidateQueries({ queryKey: ['page'] });
  const invalidatePosts = () => queryClient.invalidateQueries({ queryKey: ['posts'] });
  const invalidateAll = () => queryClient.invalidateQueries();

  return { invalidatePages, invalidatePosts, invalidateAll };
}

Resources

JavaScript SDK

Complete SDK reference

React Guide

React web integration

GitHub Repository

View source code

Content API

REST API documentation