npm package: @mapc/airtable-cms
Description: Custom React Hook for using Airtable as a CMS in MAPC web apps
Author: MAPC Digital Services
Latest version: 1.6.0

# airtable-cms

## Overview

This is a [custom React Hook](https://react.dev/learn/reusing-logic-with-custom-hooks#extracting-your-own-custom-hook-from-a-component) for using [Airtable](https://airtable.com/) as a [Content Management System (CMS)](https://en.wikipedia.org/wiki/Content_management_system), made specifically for web applications at [MAPC](https://www.mapc.org/).

## Installation

Add as a dependency: `yarn add -D @mapc/airtable-cms`

Requires `react >= 18` as a peer dependency: `yarn add 'react@>=18'`

## Usage

The `useAirtableCMS` hook is the primary export of this module. It takes a number of arguments:

### Arguments

#### Required

* `baseID` - The ID of the [Airtable Base](https://support.airtable.com/docs/airtable-bases-overview) you're using as a CMS
* `tableName` - The name of the [table](https://support.airtable.com/docs/tables-overview) you want to fetch records from
* `fieldMapping` - An object that maps [Field](https://support.airtable.com/docs/fields-overview) names to the variable names used in your app. It's also used to limit which fields are fetched from the API when requesting data
* `keyField` - The [primary field](https://support.airtable.com/docs/the-primary-field) of the table you're fetching data from (note: this should be the mapped variable name specified in `fieldMapping`, not the Field name as it appears in Airtable)

#### Optional
* `viewName` - A specific view you want to use. Default: "Grid view" (which is the default view created for each table in an Airtable Base)
* `asList` - By default, this hook returns an array of objects, but setting this to `false` will return an object (keyed by `keyField`). Default: `true`
* `sortBy` - If `asList` is `true`, you can also pass in a custom comparator function that will be used to sort the array that is returned. Default: `null`

### Return value
The hook returns an object with two attributes: `data` and `metadata`. `data` will be either a list or an object (depending on the value of the `asList` argument). `metadata` is an object with three attributes:
* `loading` - A boolean value indicating whether or not the hook is currently fetching data from the Airtable API
* `done` - A boolean value indicating whether or not the hook has finished fetching data from the Airtable API
* `error` - The [error](https://airtable.com/developers/web/api/errors) returned by the Airtable API, or `null`

Together, the `data` and `metadata` objects can be used in a variety of ways to handle different states, for example:
* `data.length === 0 && metadata.loading`: We have no cached values and we're loading data from the API (i.e. the initial state)
* `data.length > 0 && metadata.loading`: We have cached values that can be displayed, but we're checking for updates from the API
* `data.length === 0 && metadata.done`: We have no cached values to display, and the API didn't return data either (i.e. the empty state)
* `data.length === 0 && metadata.error`: We have no cached values to display, and calling the API was unsuccessful
* `data.length > 0 && metadata.error`: We have cached values we can display, but calling the API for updates was unsuccessful

Furthermore, you can use the `metadata.error` object to potentially retry requests (e.g. if the `metadata.error.code` is `429`, you've hit a rate limit, and you can wait a bit before trying again, or `502`/`503` might indicate a temporary service outage that could resolve itself after some time).

### Caching

Records fetched from Airtable are saved in [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), in order to reduce the amount of data loaded over the network, and to make already-loaded data available for immediate rendering. The hook will still make a request to the Airtable API to check for updates, [filtering records](https://airtable.com/developers/web/api/list-records#query-filterbyformula) to just those that have been updated since the last time we wrote to the local cache.

### Example

```jsx
import Spinner from "react-bootstrap/Spinner";
import { useAirtableCMS } from "@mapc/airtable-cms";

const MyComponent = () => {
  const {
    data,
    metadata: { loading },
  } = useAirtableCMS({
    baseID: "app1234xyz567",
    tableName: "Table 1",
    keyField: "name",
    fieldMapping: {
      name: "Name",
      description: "Full Description",
    },
    sortBy: (a, b) => a.name.localeCompare(b.name),
  });

  let sectionContent;
  if (loading && data.length === 0) {
    // If this is the first time we're loading content, show the loading indicator
    sectionContent = (
      <Spinner animation="border" role="status">
        <span className="visually-hidden">Loading...</span>
      </Spinner>
    );
  } else {
    sectionContent = data
      .map((record) => (
        <div key={record.name}>
          <h1>{record.name}</h1>
          <p>{record.description}</p>
        </div>
      ));
  }

  return (
    <section>
      {sectionContent}
    </section>
  );
};

export default MyComponent;
```