Extensions

Press shift + S to search API reference.

Guide

Read data from Airtable

Learn about the different models available in the SDK and how to fetch, watch for, and handle changes in data.

Extensions typically rely on data being pulled in from your base. One goal of the Blocks SDK is to make that process easy, customizable, and dynamic, so that your extension can access all the data it needs and stay up to date as it changes. This guide covers the types of data you can grab from your base and how you can use that data to power your extension.

Models

A extension runs in the context of the Airtable base in which it is installed - it can read data from and write back to that base. Some models you're already familiar with from the Airtable UI:

ModelDetails
BaseAn Airtable base. Has at least one table and at least one collaborator.
TableA table belonging to the base. Has at least one view, at least one field, and at least one record.
ViewA view belonging to a table. Within a view, a subset of the table's fields and records are visible.
FieldA field (column) belonging to a table.
RecordA record (row) belonging to a table. Has cell values corresponding to the fields of the table.

Models corresponding to the current user using the extension and the current extension installation are also available:

ModelDetails
CursorInformation about how the user of the extension is interacting with the base (active table/view, selected records/fields)
SessionInformation about the current session, including the current user.
ViewportInformation about the viewport size of the extension installation, and functionality relating to fullscreening and setting minimum/maximum size.
GlobalConfigPersistent key-value store for an extension installation (e.g. to save settings across sessions).
SettingsButtonInterface to the settings button that lives outside of the extension's viewport.

Watching data

Because Airtable is a real-time collaborative editor, the data and schema of your base can change while your extension is being used. We provide several custom React hooks that will automatically update and re-render your extension on changes. This allows you to build an extension that "live updates" with the base, and is the recommended method of handling base data.

For example, this snippet from the To-do list tutorial uses the useBase hook instead of directly importing the Base model. This means that the extension automatically updates and re-renders whenever the base name changes (as well as any other schema changes like tables being added or removed, etc).

import {initializeBlock, useBase} from '@airtable/blocks/ui';
import React from 'react';
function TodoExtension() {
const base = useBase();
return <div>{base.name}</div>;
}
initializeBlock(() => <TodoExtension />);

Other hooks (e.g., useRecords or useGlobalConfig) handle loading different sets of data from Airtable and keeping that data up to date. For performance reasons, extensions don't receive all of the base's data by default (see Advanced controls: What data is loaded by default? for more details).

Available hooks

useBase

For reading data from the base, useBase is the essential starting point: it returns the Base and re-renders whenever the base's schema changes. This includes changes to tables and fields, but excludes changes to view metadata (field order and visible fields) and record data (records in a table/view and their cell values), as those are not loaded by default.

useRecords, useRecordIds, useRecordById

useRecords is the go-to hook for interacting with record data. It loads the records from a Table or View and re-renders whenever the records are added, removed, or updated. useRecords also accepts optional RecordQueryResultOpts. These options can be used to control exactly what data is returned - only load certain fields for performance, retrieve record coloring information, and sort the records.

useRecordIds and useRecordById are useful to use together as a performance optimisation. Instead of loading and watching all record cell value data with useRecords, you can watch a list of records with useRecordIds and watch the data for certain records on demand with useRecordById.

You can also pass a RecordQueryResult selected from a Table or View. We recommend passing a Table or View as you usually don't need direct access to the query result.

useViewMetadata

The useViewMetadata hook allows you to load and access view metadata (such as visibleFields). Similar to the Record hooks, it accepts either a View or a ViewMetadataQueryResult selected from a View.

Fields are only ordered within a view, not a table: use view metadata if you want to obtain the fields visible in a particular view and their ordering. This is useful to obtain data corresponding to what a user sees in the Airtable UI.

Other hooks

Hooks corresponding to other models are also available: useSession, useViewport, useGlobalConfig and useSettingsButton.

Working with hooks

Handling deletion & conditionally using hooks

It's possible for tables, fields and views your extension is relying on to be deleted during use. Attempting to fetch a non-existent or deleted model with methods like Base#getTableByName and Base#getTableById will throw an error. To make an extension resilient to this, we recommend using IfExists() variants - these variants will return null if the model does not exist, and you can handle these cases conditionally in your code.

An important note about hooks is that they cannot be called conditionally. Instead, conditionally render an inner component that uses the hook:

import {useBase, useRecordIds} from '@airtable/blocks/ui';
import React from 'react';
function MyResilientBlock() {
const base = useBase();
const table = base.getTableByNameIfExists('My table');
if (table) {
return <RecordCount table={table} />;
} else {
return <div>Table is deleted! :(</div>;
}
}
function RecordCount({table}) {
const recordIds = useRecordIds(table);
return <div>Table has {recordIds.length} record(s)</div>;
}

Using hooks with class components

Hooks can only be used within a React function component. If you'd prefer to use a class component, you can still get the benefits of hooks by wrapping your class component in a function component and passing data as props:

import {useBase, useRecords} from '@airtable/blocks/ui';
import React from 'react';
function MyWrappedComponent() {
const base = useBase();
const table = base.getTableByName('My table');
const records = useRecords(table);
return <MyComponent table={table} records={records} />;
}
class MyComponent extends React.Component {
render() {
// You can access this.props.table and this.props.records here
return <div>...</div>;
}
}

Advanced controls

Granular control and callbacks

Under the hood, our available hooks use two more custom hooks:

  • useLoadable handles waiting for data to load asynchronously from Airtable, and unloading it when your component unmounts. It uses React Suspense to show a loading indicator over the extension while data is loading.
  • useWatchable watches a Watchable (e.g. an AbstractModel like Table or RecordQueryResult) and triggers a re-render when it changes.

You can use these yourself if you need fine-grained control. In particular, useWatchable allows you to only watch certain "watchable keys" for a model (e.g. WatchableTableKey), to avoid unnecessary re-renders of your extension.

Additionally, you can optionally pass a callback function if re-rendering on change is insufficient. The callback function will be called with the model, key, and additional information about what changed, specific to that model and key. Note: The format of the additional information is not a stable API at this time.

import {useBase, useLoadable, useWatchable} from '@airtable/blocks/ui';
import React, {useState} from 'react';
function CellValueWatcher() {
const base = useBase();
const table = base.getTableByName('My table');
const queryResult = table.selectRecords();
const [changeDetails, setChangeDetails] = useState('No changes yet');
useLoadable(queryResult);
useWatchable(queryResult, 'cellValues', (model, key, details) => {
setChangeDetails(
`${details.fieldIds.length} field(s) in ${
details.recordIds.length
} record(s) at ${Date.now()}`,
);
});
return <div>Last change: {changeDetails}</div>;
}

Both useLoadable and useWatchable accept a single model or an array of models (and a single key or array of keys). This means you can call them with an array that may change in size, or that might be empty - this is helpful for satisfying the constraint that hooks must be called at the top level (i.e. you can't nest a hook in a for loop or if statement).

import {useBase, useLoadable, useWatchable} from '@airtable/blocks/ui';
import React from 'react';
function BaseRecordCountComponent() {
const base = useBase();
const queryResults = base.tables.map((table) => table.selectRecords());
useLoadable(queryResults);
useWatchable(queryResults, 'records');
let nRecords = 0;
for (const queryResult of queryResults) {
nRecords += queryResult.records.length;
}
return (
<div>
{base.name} has {nRecords} record(s)
</div>
);
}

What data is loaded by default?

Loaded:

  • Base schema (name, tables, views, fields, collaborators)
  • Session information (user, permission level)
  • Cursor information for selected table and view

Not loaded:

  • Records
  • View metadata (field order, visible fields)
  • Cursor information for selected records and fields

Data that is not loaded by default is part of a AbstractModelWithAsyncData (e.g. RecordQueryResult). You can use useLoadable to load these models, or use a higher level hook.

Manual loading and watching

We strongly encourage use of the SDK's hooks to handle loading and watching data. (If you want to use class components, please see above section Using hooks with class components.)

If necessary, it is possible to manually load and unload an AbstractModelWithAsyncData yourself with loadDataAsync and unloadData. Similarly, you can watch and unwatch a Watchable. When doing this, it's important to use unloadData to unload data after you don't need it anymore and unwatch to clean up a subscription when unmounting the component that called watch (useLoadable and useWatchable handle this under the hood).