Contentful-local-schema: A JS/TS library for syncing Contentful content and querying it locally

Hi All!

We at Watermark Community Church have started getting into building apps using React Native and Expo. We love using Contentful content to power our apps!

One of my pet peeve is apps that are really just installable web pages. Why should my app require the internet just to show me the main screen? For our most recent app, we realized that the entirety of our content was only about 10MB of text and links. That could easily fit in RAM! So we built a prototype library to allow you to query your contentful space in RAM, after syncing it via the Sync API. The only API calls this library makes is to the Sync API on the CDN, regardless of how often you query it. Your contentful space gets downloaded and stored in AsyncStorage and re-hydrated when the app is opened, so after you’ve loaded your content once you really only need updates from that point on.

We’ve been using this to power our Church Leaders Conference app and it’s so fast that we’ve been able to eliminate almost all loading states!

It has both React and Apollo GraphQL integrations, so you can use it with your favorite flavor.

Hope you like it, and we would welcome pull requests!

Here’s an example of how it’s used in our app:

// contentful-provider.tsx
import {
  createSimpleClient,
  InMemoryDataSource,
  addBackup,
  addSync
} from 'contentful-local-schema'
import {
  LocalSchemaProvider
} from 'contentful-local-schema/react'
import AsyncStorage from '@react-native-async-storage/async-storage'

import Constants from 'expo-constants';

const space = Constants.expoConfig?.extra?.contentful?.spaceId || '...space...';
const environmentId = Constants.expoConfig?.extra?.contentful?.environmentId || 'master';
const accessToken = Constants.expoConfig?.extra?.contentful?.accessToken

const client = createSimpleClient({
  accessToken,
  space,
  environmentId,
})

const dataSource = new InMemoryDataSource()
addSync(dataSource, client)
addBackup(dataSource,
  AsyncStorage,
  `contentful/${space}/${environmentId}`)

export function ContentfulProvider({children}: React.PropsWithChildren) {

  return (
    <LocalSchemaProvider dataSource={dataSource} >
      {children}
    </LocalSchemaProvider>
  )
}

// home.tsx
import React, { useCallback, useRef } from 'react';
import { useFindEntry } from 'contentful-local-schema/react';

export function Home({ route, navigation, ...props }) {
  const [conference, { loading, refreshing, error }, refresh] = useFindEntry('7pE6FgpSVhbx3u105MMZFz', { include: 4 })

  const items = conference?.fields.announcements || []

  const renderItem = useCallback(({item}) => {
    return <AnnouncementCard entry={item} />
  }, [])

  return <BackgroundView>
        <SafeAreaView edges={['top', 'left', 'right']}>
          <FlatList
            data={items}
            onRefresh={refresh}
            refreshing={refreshing}
            renderItem={renderItem}
          />

        </SafeAreaView>
    </BackgroundView>
}
1 Like