> ## 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 Native

> Build content-driven React Native applications with ButterCMS for iOS and Android. Fetch 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>

## Installation

<Tabs>
  <Tab title="npm">
    ```bash theme={null}
    npm install buttercms react-native-render-html
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn add buttercms react-native-render-html
    ```
  </Tab>

  <Tab title="pnpm">
    ```bash theme={null}
    pnpm add buttercms react-native-render-html
    ```
  </Tab>
</Tabs>

For environment variables, install react-native-config:

```bash theme={null}
npm install react-native-config
```

## Initialize the client

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    // lib/buttercms.ts
    import Butter from 'buttercms';
    import Config from 'react-native-config';

    const butter = Butter(Config.BUTTERCMS_API_TOKEN!);

    export default butter;
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    // lib/buttercms.js
    import Butter from 'buttercms';
    import Config from 'react-native-config';

    const butter = Butter(Config.BUTTERCMS_API_TOKEN);

    export default butter;
    ```
  </Tab>
</Tabs>

Add to `.env`:

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

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

## Custom hooks

<Tabs>
  <Tab title="useState Hooks">
    ```typescript theme={null}
    // hooks/useButter.ts
    import { useState, useEffect, useCallback } from 'react';
    import butter from '../lib/buttercms';

    interface Author {
      slug: string;
      first_name: string;
      last_name: string;
    }

    interface Post {
      slug: string;
      title: string;
      summary: string;
      body?: string;
      featured_image: string | null;
      published: string | null;
      author: Author;
    }

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

    interface ButterPage<Fields> {
      slug: string;
      page_type: string;
      fields: Fields;
    }

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

    export function usePage<Fields extends object>(pageType: string, slug: string): UsePageResult<Fields> {
      const [page, setPage] = useState<ButterPage<Fields> | 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<Fields>(pageType, slug);
          if (!response.data?.data) {
            throw new Error('Failed to load page from ButterCMS');
          }
          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, unknown> | 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);
            if (!response.data?.data) {
              throw new Error('Failed to load collection from ButterCMS');
            }
            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<Post[]>([]);
      const [meta, setMeta] = useState<Meta | null>(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState<Error | null>(null);

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

      return { posts, meta, loading, error };
    }

    export function usePost(slug: string) {
      const [post, setPost] = useState<Post | null>(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 ?? null);
          } catch (err) {
            setError(err as Error);
          } finally {
            setLoading(false);
          }
        }
        fetchPost();
      }, [slug]);

      return { post, loading, error };
    }
    ```
  </Tab>

  <Tab title="React Query">
    ```typescript theme={null}
    // hooks/useButter.ts (with React Query)
    import { useQuery } from '@tanstack/react-query';
    import butter from '../lib/buttercms';

    interface Author {
      slug: string;
      first_name: string;
      last_name: string;
    }

    interface Post {
      slug: string;
      title: string;
      summary: string;
      body?: string;
      featured_image: string | null;
      published: string | null;
      author: Author;
    }

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

    export function usePage<Fields extends object>(pageType: string, slug: string) {
      return useQuery({
        queryKey: ['page', pageType, slug],
        queryFn: async () => {
          const response = await butter.page.retrieve<Fields>(pageType, slug);
          if (!response.data?.data) {
            throw new Error('Failed to load page from ButterCMS');
          }
          return response.data.data;
        },
        staleTime: 1000 * 60 * 5, // 5 minutes
      });
    }

    export function useCollection(keys: string[]) {
      return useQuery({
        queryKey: ['collection', ...keys],
        queryFn: async () => {
          const response = await butter.content.retrieve(keys);
          if (!response.data?.data) {
            throw new Error('Failed to load collection from ButterCMS');
          }
          return response.data.data;
        },
        staleTime: 1000 * 60 * 5,
      });
    }

    export function usePosts(page = 1, pageSize = 10) {
      return useQuery({
        queryKey: ['posts', page, pageSize],
        queryFn: async () => {
          const response = await butter.post.list({ page, page_size: pageSize });
          if (!response.data?.data) {
            throw new Error('Failed to load posts from ButterCMS');
          }
          return {
            posts: response.data.data as Post[],
            meta: (response.data.meta ?? null) as Meta | null,
          };
        },
        staleTime: 1000 * 60 * 5,
      });
    }

    export function usePost(slug: string) {
      return useQuery({
        queryKey: ['post', slug],
        queryFn: async () => {
          const response = await butter.post.retrieve(slug);
          if (!response.data?.data) {
            throw new Error('Failed to load post from ButterCMS');
          }
          return response.data.data as Post;
        },
        staleTime: 1000 * 60 * 5,
      });
    }
    ```
  </Tab>
</Tabs>

## Pages

<Tabs>
  <Tab title="useState">
    ```tsx theme={null}
    // 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;
    }

    type LandingPageRoute = { params?: { slug?: string } };

    export default function LandingPage({ route }: { route: LandingPageRoute }) {
      const { slug = 'home' } = route.params || {};
      const { page, loading, error } = usePage<PageFields>('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 },
    });
    ```
  </Tab>

  <Tab title="React Query">
    ```tsx theme={null}
    // 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;
    }

    type LandingPageRoute = { params?: { slug?: string } };

    export default function LandingPage({ route }: { route: LandingPageRoute }) {
      const { slug = 'home' } = route.params || {};
      const { data: page, isLoading, isError } = usePage<PageFields>('landing_page', slug);
      const { width } = useWindowDimensions();

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

      if (isError || !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>
      );
    }
    ```
  </Tab>
</Tabs>

## Collections

<Tabs>
  <Tab title="useState">
    ```tsx theme={null}
    // 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';

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

    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>
        );
      }

      const brands = data?.brands as Brand[] | undefined;

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

      return (
        <FlatList
          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 },
    });
    ```
  </Tab>

  <Tab title="React Query">
    ```tsx theme={null}
    // screens/BrandsScreen.tsx
    import React from 'react';
    import { FlatList, Text, Image, View, StyleSheet, ActivityIndicator } from 'react-native';
    import { useCollection } from '../hooks/useButter';

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

    export default function BrandsScreen() {
      const { data, isLoading, isError } = useCollection(['brands']);

      const brands = data?.brands as Brand[] | undefined;

      if (isLoading) {
        return <ActivityIndicator size="large" style={styles.centered} />;
      }

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

      return (
        <FlatList
          data={brands}
          keyExtractor={(item, index) => `brand-${index}`}
          renderItem={({ item }) => (
            <View style={styles.brandCard}>
              {item.logo && <Image source={{ uri: item.logo }} style={styles.logo} />}
              <Text style={styles.brandName}>{item.name}</Text>
            </View>
          )}
        />
      );
    }
    ```
  </Tab>
</Tabs>

## Dynamic components

### Component Renderer

```tsx theme={null}
// 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 HeroFields {
  headline: string;
  subheadline: string;
  image?: string;
  button_label?: string;
  button_url?: string;
}

interface FeaturesFields {
  headline: string;
  features: { title: string; description: string; icon?: string }[];
}

interface CTAFields {
  headline: string;
  button_label: string;
  button_url: string;
}

export type PageComponent =
  | { type: 'hero'; fields: HeroFields }
  | { type: 'features'; fields: FeaturesFields }
  | { type: 'cta'; fields: CTAFields };

interface Props {
  components: PageComponent[];
}

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

```tsx theme={null}
// 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

<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>

<Tabs>
  <Tab title="useState">
    ```tsx theme={null}
    // 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';
    import type { PageComponent } from '../components/ComponentRenderer';

    interface ComponentPageFields {
      body: PageComponent[];
    }

    type ComponentPageRoute = { params: { slug: string } };

    export default function ComponentPage({ route }: { route: ComponentPageRoute }) {
      const { slug } = route.params;
      const { page, loading, error } = usePage<ComponentPageFields>('component_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' },
    });
    ```
  </Tab>

  <Tab title="React Query">
    ```tsx theme={null}
    // screens/ComponentPage.tsx
    import React from 'react';
    import { ScrollView, ActivityIndicator, View, Text } from 'react-native';
    import { usePage } from '../hooks/useButter';
    import ComponentRenderer from '../components/ComponentRenderer';
    import type { PageComponent } from '../components/ComponentRenderer';

    interface ComponentPageFields {
      body: PageComponent[];
    }

    type ComponentPageRoute = { params: { slug: string } };

    export default function ComponentPage({ route }: { route: ComponentPageRoute }) {
      const { slug } = route.params;
      const { data: page, isLoading, isError } = usePage<ComponentPageFields>('component_page', slug);

      if (isLoading) {
        return <ActivityIndicator size="large" />;
      }

      if (isError || !page) {
        return <Text>Page not found</Text>;
      }

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

## Blog

<Tabs>
  <Tab title="Blog List">
    <Tabs>
      <Tab title="useState">
        ```tsx theme={null}
        // screens/BlogList.tsx
        import React from 'react';
        import {
          FlatList,
          Text,
          TouchableOpacity,
          View,
          Image,
          StyleSheet,
          ActivityIndicator,
        } from 'react-native';
        import { usePosts } from '../hooks/useButter';

        type BlogListNavigation = {
          navigate: (screen: string, params: { slug: string }) => void;
        };

        export default function BlogList({ navigation }: { navigation: BlogListNavigation }) {
          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' },
        });
        ```
      </Tab>

      <Tab title="React Query">
        ```tsx theme={null}
        // screens/BlogList.tsx
        import React from 'react';
        import { FlatList, Text, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
        import { usePosts } from '../hooks/useButter';

        type BlogListNavigation = {
          navigate: (screen: string, params: { slug: string }) => void;
        };

        export default function BlogList({ navigation }: { navigation: BlogListNavigation }) {
          const { data, isLoading } = usePosts();

          if (isLoading) {
            return <ActivityIndicator size="large" />;
          }

          return (
            <FlatList
              data={data?.posts || []}
              keyExtractor={(item) => item.slug}
              renderItem={({ item }) => (
                <TouchableOpacity
                  style={styles.postCard}
                  onPress={() => navigation.navigate('BlogPost', { slug: item.slug })}
                >
                  <Text style={styles.postTitle}>{item.title}</Text>
                  <Text style={styles.postAuthor}>
                    By {item.author.first_name} {item.author.last_name}
                  </Text>
                </TouchableOpacity>
              )}
            />
          );
        }
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Blog Post">
    <Tabs>
      <Tab title="useState">
        ```tsx theme={null}
        // screens/BlogPost.tsx
        import React from 'react';
        import {
          ScrollView,
          Text,
          Image,
          View,
          StyleSheet,
          ActivityIndicator
        } from 'react-native';
        import { usePost } from '../hooks/useButter';
        import RenderHtml from 'react-native-render-html';
        import { useWindowDimensions } from 'react-native';

        type BlogPostRoute = { params: { slug: string } };

        export default function BlogPost({ route }: { route: BlogPostRoute }) {
          const { slug } = route.params;
          const { post, loading, error } = usePost(slug);
          const { width } = useWindowDimensions();

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

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

          return (
            <ScrollView style={styles.container}>
              <Text style={styles.title}>{post.title}</Text>
              <Text style={styles.author}>
                By {post.author.first_name} {post.author.last_name}
              </Text>
              <Text style={styles.date}>
                {post.published ? new Date(post.published).toLocaleDateString() : ''}
              </Text>

              {post.featured_image && (
                <Image
                  source={{ uri: post.featured_image }}
                  style={styles.featuredImage}
                  resizeMode="cover"
                />
              )}

              <RenderHtml
                contentWidth={width - 32}
                source={{ html: post.body ?? '' }}
              />
            </ScrollView>
          );
        }

        const styles = StyleSheet.create({
          container: { flex: 1, padding: 16 },
          centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
          error: { color: '#ff3b30' },
          title: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
          author: { fontSize: 14, color: '#666' },
          date: { fontSize: 12, color: '#999', marginBottom: 16 },
          featuredImage: { width: '100%', height: 200, borderRadius: 8, marginBottom: 16 },
        });
        ```
      </Tab>

      <Tab title="React Query">
        ```tsx theme={null}
        // screens/BlogPost.tsx
        import React from 'react';
        import { ScrollView, Text, Image, ActivityIndicator } from 'react-native';
        import { usePost } from '../hooks/useButter';
        import RenderHtml from 'react-native-render-html';

        type BlogPostRoute = { params: { slug: string } };

        export default function BlogPost({ route }: { route: BlogPostRoute }) {
          const { slug } = route.params;
          const { data: post, isLoading, isError } = usePost(slug);

          if (isLoading) return <ActivityIndicator size="large" />;
          if (isError || !post) return <Text>Post not found</Text>;

          return (
            <ScrollView style={{ padding: 16 }}>
              <Text style={{ fontSize: 24, fontWeight: 'bold' }}>{post.title}</Text>
              <RenderHtml source={{ html: post.body ?? '' }} />
            </ScrollView>
          );
        }
        ```
      </Tab>
    </Tabs>
  </Tab>
</Tabs>

## Navigation

```tsx theme={null}
// 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

```typescript theme={null}
// 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
      gcTime: 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

<CardGroup cols={2}>
  <Card title="JavaScript SDK" icon="js" href="../sdks/javascript-sdk">
    Complete SDK reference
  </Card>

  <Card title="React Guide" icon="react" href="./react">
    React web integration
  </Card>

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

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