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:
Model | Details |
---|---|
Base | An Airtable base. Has at least one table and at least one collaborator. |
Table | A table belonging to the base. Has at least one view, at least one field, and at least one record. |
View | A view belonging to a table. Within a view, a subset of the table's fields and records are visible. |
Field | A field (column) belonging to a table. |
Record | A 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:
Model | Details |
---|---|
Cursor | Information about how the user of the extension is interacting with the base (active table/view, selected records/fields) |
Session | Information about the current session, including the current user. |
Viewport | Information about the viewport size of the extension installation, and functionality relating to fullscreening and setting minimum/maximum size. |
GlobalConfig | Persistent key-value store for an extension installation (e.g. to save settings across sessions). |
SettingsButton | Interface 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 herereturn <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 aWatchable
(e.g. anAbstractModel
likeTable
orRecordQueryResult
) 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).