# Build a to-do list extension

Learn how to build a to-do list extension that shows a list of tasks from a table in this 4-part tutorial.

## What your extension will look like

![Extension showing a to-do list](https://static.airtable.com/blocks/images/todo-1-overview.gif)

Here's a breakdown of what we'll cover in each part:

-   Part 1: show the list of tasks.
-   Part 2: allow the user to pick which table they want to show tasks from.
-   Part 3: remember the user's choices for which table they want to show tasks from.
-   Part 4: integrate with a checkbox field to manage the "completed" status of each task.

**We'll assume you've already read the [getting started guide](https://airtable.com/developers/extensions/guides/getting-started.md).**

Let's get started!

## Part 1

Copy [this base](https://airtable.com/shrEs0vjKrBR8qjBy), or go to an existing base. If you're using
an existing base, make sure it has a table called "Tasks" — if it doesn't, create one or rename one
of the existing tables.

Open the extensions panel, click "Install an extension", then click "Build a custom extension". Follow the onscreen
instructions to set up the extension.

![Airtable user going through the build an extension flow](https://static.airtable.com/blocks/images/todo-2-setup-apps-rename.gif)

Open `frontend/index.js` in your code editor and let's start writing some code! It should look like
this:

```js
import {initializeBlock} from '@airtable/blocks/ui';
import React from 'react';

function TodoExtension() {
    // YOUR CODE GOES HERE
    return <div>Hello world 🚀</div>;
}

initializeBlock(() => <TodoExtension />);
```

As a first step, let's make the extension show the base name instead of "Hello world." To get
information about the base that the extension is running inside, we need the `base` object.

We can import it directly using `import {base} from '@airtable/blocks'`, but in this case it's
better to use a [React hook](https://reactjs.org/docs/hooks-intro.html) to get the base object. By
using a hook, our component will automatically update when the relevant information changes. We'll
use the `useBase` hook, which will cause the `TodoExtension` component to re-render whenever the base
name changes (it will also re-render when tables, fields, and views are created, updated, or deleted
and when the current user's permission level changes).

Change `index.js` to look like this:

```diff
import {
    initializeBlock,
+   useBase,
} from '@airtable/blocks/ui';
import React from 'react';

function TodoExtension() {
+   const base = useBase();

    return (
-       <div>Hello world 🚀</div>
+       <div>{base.name}</div>
    );
}

initializeBlock(() => <TodoExtension />);
```

The extension should show the base's name and if you rename the base, the extension should automatically
update to show the new name!

### Showing the number of records

Now we'll change the extension to show the number of records in the "Tasks" table instead of the base
name.

Change `index.js` to look like this:

```diff
import {
    initializeBlock,
    useBase,
+   useRecords,
} from '@airtable/blocks/ui';
import React from 'react';

function TodoExtension() {
    const base = useBase();
+   const table = base.getTableByName('Tasks');

+   const records = useRecords(table);

    return (
-       <div>{base.name}</div>
+       <div>Number of tasks: {records.length}</div>
    );
}

initializeBlock(() => <TodoExtension />);
```

Try creating and deleting records in the table! You should see the number of tasks in the extension
update.

Let's walk through the 3 lines that changed:

```js
const table = base.getTableByName('Tasks');
```

We're asking the base to give us the Table object corresponding to the table called "Tasks". The
extension will crash if there isn't a table called "Tasks", and we'll see how to deal with this later in
this tutorial. For now we'll assume there will always be a table called "Tasks".

```js
const records = useRecords(table);
```

Then we use a new hook, called `useRecords`, to connect our `TodoExtension` component to the records
inside the `Table`. Any time records are created, deleted, or updated in the table, our component
will automatically re-render.

`records` will be an array of `Record` objects. Since it's an array, we can use `records.length` to
get the number of records.

```js
return <div>Number of tasks: {records.length}</div>;
```

### Showing the name of the records

Let's change the extension to show the primary cell value of all the records, instead of just the count.

Change `index.js` to look like this:

```diff
import {
    initializeBlock,
    useBase,
    useRecords,
} from '@airtable/blocks/ui';
import React from 'react';

function TodoExtension() {
    const base = useBase();
    const table = base.getTableByName('Tasks');

    const records = useRecords(table);

+   const tasks = records.map(record => {
+       return (
+           <div key={record.id}>
+               {record.name || 'Unnamed record'}
+           </div>
+       );
+   });

    return (
-       <div>Number of tasks: {records.length}</div>
+       <div>{tasks}</div>
    );
}

initializeBlock(() => <TodoExtension />);
```

We're mapping over the `Record` objects in the `records` array to create an array of `<div>`
elements, one `<div>` per record.

Since we're rendering a list, we include a unique key for each element by using the record's ID.
[Learn more about why React needs keys in lists of elements here.](https://reactjs.org/docs/lists-and-keys.html)

Inside each `<div>`, we render the primary cell value of the record. Instead of using
`record.primaryCellValue`, we use `record.name` to automatically handle
converting cell values to string (for example, the table's primary field might be a number field).

`record.name` might be an empty string, in which case we want to show "Unnamed
record" in our list.

You should now see the primary cell value of the records in the table in your extension, and if you edit
any of the names from outside the extension, the extension will automatically update to show the latest
names.

If you want to get the cell values from other fields, you can use `record.getCellValue()` or
`record.getCellValueAsString()`.

### Expanding records

Let's add a button to allow users to expand the record and edit its contents.

Before we do that, let's refactor our extension a little bit. It'll make it easier to add functionality
and keep our code more readable if we create a separate `Task` component, instead of continuing to
add code to the top-level `TodoExtension` component:

```diff
import {
    initializeBlock,
    useBase,
    useRecords,
} from '@airtable/blocks/ui';
import React from 'react';

function TodoExtension() {
    const base = useBase();
    const table = base.getTableByName('Tasks');

    const records = useRecords(table);

    const tasks = records.map(record => {
-       return (
-           <div key={record.id}>
-               {record.name || 'Unnamed record'}
-           </div>
-       );
+       return <Task key={record.id} record={record} />;
    });

    return (
        <div>{tasks}</div>
    );
}

+function Task({record}) {
+    return (
+        <div>
+            {record.name || 'Unnamed record'}
+        </div>
+    );
+}

initializeBlock(() => <TodoExtension />);
```

Our new Task component takes a prop called `record` and renders its name, or "Unnamed record" if its
name is blank.

Now we can change our `Task` component to expand the record when the user
clicks on a nearby button. We'll do that by using the `TextButton` component
and giving it an `onClick` handler that calls `expandRecord()`. To keep this button minimal, we'll label it with an icon instead of text.

```diff
import {
    initializeBlock,
    useBase,
    useRecords,
+   expandRecord,
+   TextButton,
} from '@airtable/blocks/ui';
import React from 'react';

function TodoExtension() { /* No changes */ }

function Task({record}) {
    return (
        <div>
            {record.name || 'Unnamed record'}
+           <TextButton
+               icon="expand"
+               aria-label="Expand record"
+               onClick={() => {
+                   expandRecord(record);
+               }}
+           />
        </div>
    );
}

initializeBlock(() => <TodoExtension />);
```

Clicking on the expand button in your extension will expand the record, allowing
you to edit its name and other fields. You can also use the `Tab` key
on your keyboard to focus on one of the buttons and then press `Enter` to expand the record.

### A little style

One last thing to do before we wrap up Part 1: let's add some CSS styles to make our extension look
polished! Feel free to tweak the below styles to customize your extension.

```diff
function Task({record}) {
    return (
-       <div>
+       <div
+           style={{
+               display: 'flex',
+               alignItems: 'center',
+               justifyContent: 'space-between',
+               fontSize: 18,
+               padding: 12,
+               borderBottom: '1px solid #ddd',
+           }}
+       >
            {record.name || 'Unnamed record'}
            <TextButton
                icon="expand"
                aria-label="Expand record"
+               variant="dark"
                onClick={() => {
                    expandRecord(record);
                }}
            />
        </div>
    );
}
```

Congratulations on finishing Part 1! You should have an extension that looks like this:

![Extension with a to-do list showing records from the table](https://static.airtable.com/blocks/images/todo-3-wip.gif)

## Part 2

The extension we made in Part 1 has a pretty big limitation: it only works if the base has a table
called "Tasks." Try renaming the table and you'll see the extension crash.

Let's change the extension to let the user pick which table they want to use to show their tasks!

### Don't crash when there's no table

First, we need to change `TodoExtension` to handle the case where there is no table selected. Instead of
crashing, we'll change it to show a blank screen:

```diff
function TodoExtension() {
    const base = useBase();
-   const table = base.getTableByName('Tasks');
+   const table = base.getTableByNameIfExists('Tasks');

    const records = useRecords(table);

-   const tasks = records.map(record => {
-       return <Task key={record.id} record={record} />;
-   });
+   const tasks = records ? records.map(record => {
+       return <Task key={record.id} record={record} />;
+   }) : null;

    return (
        <div>{tasks}</div>
    );
}
```

`base.getTableByName` will crash the extension if there's no table in the base with the specified name.
`base.getTableByNameIfExists` will return null instead of crashing if there's no table with the
specified name.

Now that `table` might be null, we need to be careful when using it. It's okay to call `useRecords` with `null`,
but the returned `records` will also be null. We use a ternary expression to make sure we're only calling
`records.map` when `records` is not null. If `records` is null, we won't try rendering any `Task` components.

```js
const tasks = records ? records.map(record => (
    <Task key={record.id} record={record} />
)) : null;
```

_Aside:_ the above line is equivalent to:

```js
let tasks;
if (records) {
    tasks = records.map(record => {
        return <Task key={record.id} record={record} />;
    });
} else {
    tasks = null;
}
```

We prefer to use ternary expressions for these null checks because they help make the code more
concise, but you can write out `if` statements if you prefer!

Now the extension should work as before, but if you rename the "Tasks" table, the extension should show a
blanks screen instead of crashing. Change the name of the table back to "Tasks" and the records
should appear again inside the extension.

### Storing the selected table in state

Right now we're hard-coding "Tasks" as the table name the extension will use. To let the user specify
which table they want to use, we'll store the table name in the `TodoExtension` component's state with
[React's built-in useState hook:](https://reactjs.org/docs/hooks-state.html)

```diff
import {
    initializeBlock,
    useBase,
    useRecords,
    expandRecord,
    TextButton,
} from '@airtable/blocks/ui';
-import React from 'react';
+import React, {useState} from 'react';

function TodoExtension() {
    const base = useBase();

+   const [tableName, setTableName] = useState('Tasks');

-   const table = base.getTableByNameIfExists('Tasks');
+   const table = base.getTableByNameIfExists(tableName);

    const records = useRecords(table);

    const tasks = records ? records.map(record => (
        <Task key={record.id} record={record} />
    )) : null;

    return (
        <div>{tasks}</div>
    );
}

function Task({record}) { /* No change */ }

initializeBlock(() => <TodoExtension />);
```

We're still hard-coding "Tasks" as the initial table name, but if we call `setTableName` with the
name of another table, the extension will switch to show records from that table. To make sure make sure
that works, we need to add some way for the user to pick a table. The Blocks SDK includes a
`TablePicker` component we can use!

```diff
import {
    initializeBlock,
    useBase,
    useRecords,
    expandRecord,
+   TablePicker,
    TextButton,
} from '@airtable/blocks/ui';
import React, {useState} from 'react';

function TodoExtension() {
    const base = useBase();

    const [tableName, setTableName] = useState('Tasks');

    const table = base.getTableByNameIfExists(tableName);

    const records = useRecords(table);

    const tasks = records ? records.map(record => (
        <Task key={record.id} record={record} />
    )) : null;

    return (
        <div>
+           <TablePicker
+               table={table}
+               onChange={newTable => {
+                   setTableName(newTable.name);
+               }}
+           />
            {tasks}
        </div>
    );
}

function Task({record}) { /* No change */ }

initializeBlock(() => <TodoExtension />);
```

Now there should be a dropdown that lets you pick between different tables in the base (create a new
table if you only have 1)!

![Block showing a to-do list with a table picker](https://static.airtable.com/blocks/images/todo-4-table-select.gif)

### Using table ID instead of table name

It's definitely an improvement that the user can pick which table to use. But if anyone renames the
table, the extension will stop showing the records until you pick the table again. We can avoid this by
using the table's ID instead of its name. The table ID won't change when the table gets renamed.

```diff
function TodoExtension() {
    const base = useBase();

-   const [tableName, setTableName] = useState('Tasks');
+   const [tableId, setTableId] = useState(null);

-   const table = base.getTableByNameIfExists(tableName);
+   const table = base.getTableByIdIfExists(tableId);

    const records = useRecords(table);

    const tasks = records ? records.map(record => (
        <Task key={record.id} record={record} />
    )) : null;

    return (
        <div>
            <TablePicker
                table={table}
                onChange={newTable => {
-                   setTableName(newTable.name);
+                   setTableId(newTable.id);
                }}
            />
            {tasks}
        </div>
    );
}
```

Now try renaming the selected table. The extension will continue to show records from that table!

## Part 3

The user can pick which table they want to show tasks from, but the extension doesn't remember their
choice. Every time they load the extension, they start with an empty list until they pick the table. It
would be better if the extension remembered the user's choice!

### Storing configuration

Each extension installation has a storage mechanism called `globalConfig` where you can store
configuration information. The contents of `globalConfig` will be synced in real-time to all logged
in users of that extension installation. Because any base collaborator can read from it, you shouldn't
store sensitive data here.

Airtable's existing extensions, like _Page designer_ and _Chart_, make heavy use of global config, and
your extensions will likely do the same. For example, Airtable's Chart extension lets you choose which kind
of chart you want to use (bar chart, pie chart, etc) and it stores the chart type in `globalConfig`.

Let's change the extension to store the selected table's ID in `globalConfig` instead of the `TodoExtension`
component's state:

```diff
import {
    initializeBlock,
    useBase,
    useRecords,
+   useGlobalConfig,
    expandRecord,
    TablePicker,
    TextButton,
} from '@airtable/blocks/ui';
import React from 'react';

function TodoExtension() {
    const base = useBase();

-   const [tableId, setTableId] = useState(null);
+   const globalConfig = useGlobalConfig();
+   const tableId = globalConfig.get('selectedTableId');

    const table = base.getTableByIdIfExists(tableId);

    const records = useRecords(table);

    const tasks = records ? records.map(record => (
        <Task key={record.id} record={record} />
    )) : null;

    return (
        <div>
            <TablePicker
                table={table}
                onChange={newTable => {
-                   setTableId(newTable.id);
+                   globalConfig.setAsync('selectedTableId', newTable.id);
                }}
            />
            {tasks}
        </div>
    );
}

function Task({record}) { /* No changes */ }

initializeBlock(() => <TodoExtension />);
```

Let's walk through the lines that changed:

```js
const globalConfig = useGlobalConfig();
```

With the `useGlobalConfig` hook, we can have our `TodoExtension` component access data in `globalConfig`
and automatically re-render when any of that data changes.

```js
const tableId = globalConfig.get('selectedTableId');
```

Previously, the table ID was stored in the component state with the `useState` hook. Now we're
storing it in `globalConfig`, so we get its value by calling `globalConfig.get()`. We're choosing to
use "selectedTableId" as the key in globalConfig, but you could call it whatever you want—it just
has to match the key you pass to `globalConfig.setAsync()` below.

```js
globalConfig.setAsync('selectedTableId', newTable.id);
```

When the user picks a new table from the `TablePicker`, we use `globalConfig.setAsync()` to update
the table ID that's stored in `globalConfig`.

Values in `globalConfig` can be strings, numbers, booleans, null, arrays, and plain objects—anything
that can be encoded as JSON. This means we can't store the table object directly in `globalConfig`,
so we store its ID instead, which is a string.

Now when you pick the table, it'll be saved. If you reload the extension installation, it'll remember
the table you were using. Much better!

### Permissions

There's a bug in the changes we just made. Read-only and comment-only collaborators aren't allowed
to update globalConfig, so if they try changing the selected table our extension will crash. You can try
this out by clicking "Simulate," then choosing "Read" or "Comment" from the dropdown:

![Airtable user showing simulated permissions in a extension](https://static.airtable.com/blocks/images/todo-5-permissions.gif)

We could fix this by disabling the `TablePicker` if the user doesn't have permission to change
`globalConfig` by using `globalConfig.checkPermissionsForSetPaths()`.

But there's an easier way! The `TablePicker` component has a sibling component called
`TablePickerSynced` which automatically reads and writes to `globalConfig` with the proper
permission checks. Let's switch to that.

```diff
import {
    initializeBlock,
    useBase,
    useRecords,
    useGlobalConfig,
    expandRecord,
-   TablePicker,
+   TablePickerSynced,
    TextButton,
} from '@airtable/blocks/ui';
import {globalConfig} from '@airtable/blocks';
import React, {useState} from 'react';

function TodoExtension() {
    const base = useBase();

    const globalConfig = useGlobalConfig();
    const tableId = globalConfig.get('selectedTableId');

    const table = base.getTableByIdIfExists(tableId);

    const records = useRecords(table);

    const tasks = records ? records.map(record => (
        <Task key={record.id} record={record} />
    )) : null;

    return (
        <div>
-           <TablePicker
-               table={table}
-               onChange={newTable => {
-                   globalConfig.setAsync('selectedTableId', newTable.id);
-               }}
-           />
+           <TablePickerSynced globalConfigKey="selectedTableId" />
            {tasks}
        </div>
    );
}

function Task({record}) { /* No change */ }

initializeBlock(() => <TodoExtension />);
```

Instead of passing a `table` and an `onChange` prop, we tell `TablePickerSynced` which key in
`globalConfig` it should read from and write to using the `globalConfigKey` prop.

Now if you try simulating a "Read" or "Comment" permission level, the table picker will become
disabled.

## Part 4

As the user accomplishes their tasks, they'll want some way to note their
achievement and keep track of what they still have left to do. This is a "to
do" application, after all! Let's extend the list so that users can see and
update which tasks are complete.

### Tracking completed tasks

Each record in the base has a single line text field that describes a task. It
should also have a checkbox field that denotes when a task is complete. If your
table doesn't have a checkbox field yet, you should add one now so that we can
visualize this task status in the extension.

Our extension needs to know which field in the table is the checkbox field. Rather
than assuming that we know the exact name or ID of this field, we'll apply the
same pattern we used to make the table name configurable. This time, we'll use
the `FieldPickerSynced` component to store the field ID in `globalConfig`. The
`Task` component will need this field ID, so we'll supply it as a prop, but
we'll wait to update the component until the next step.

We'll also add an extra check to verify that the field still exists. If someone
deletes the field, we don't want the task trying to lookup a cell value for a
non-existent field!

```diff
import {
+   FieldPickerSynced,
    initializeBlock,
    useBase,
    useRecords,
    useGlobalConfig,
    expandRecord,
    TablePickerSynced,
    TextButton,
} from '@airtable/blocks/ui';
import {globalConfig} from '@airtable/blocks';
import React, {useState} from 'react';

function TodoExtension() {
    const base = useBase();

    const globalConfig = useGlobalConfig();
    const tableId = globalConfig.get('selectedTableId');
+   const completedFieldId = globalConfig.get('completedFieldId');

    const table = base.getTableByIdIfExists(tableId);
+   const completedField = table ? table.getFieldByIdIfExists(completedFieldId) : null;

    const records = useRecords(table);

-   const tasks = records ? records.map(record => (
+   const tasks = records && completedField ? records.map(record => (
-       <Task key={record.id} record={record} />
+       <Task key={record.id} record={record} completedFieldId={completedFieldId} />
   )) : null;

    return (
        <div>
            <TablePickerSynced globalConfigKey="selectedTableId" />
+           <FieldPickerSynced table={table} globalConfigKey="completedFieldId" />
            {tasks}
        </div>
    );
}

function Task({record}) { /* No change */ }

initializeBlock(() => <TodoExtension />);
```

Now, the user is able to tell us which field describes whether a task is
complete or not. That's nice, but we'll need to update the `Task` component
before they can see the field's effect.

```diff
-function Task({record}) {
+function Task({record, completedFieldId}) {
+   const label = record.name || 'Unnamed record';
+
    return (
        <div
            style={{
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'space-between',
                fontSize: 18,
                padding: 12,
                borderBottom: '1px solid #ddd',
            }}
        >
-           {record.name || 'Unnamed record'}
+           {record.getCellValue(completedFieldId) ? <s>{label}</s> : label}
            <TextButton
                icon="expand"
                aria-label="Expand record"
                variant="dark"
                onClick={() => {
                    expandRecord(record);
                }}
            />
        </div>
    );
}
```

If the task is complete, we'll wrap its name in an `<s>` (strikethrough)
element, so that browsers and screen readers know that it is no longer
relevant. Otherwise, we can render the task name as a normal string. Here's
a screen shot of what the extension should look like now:

![Todo list extension marks completed tasks as complete with strikethroughs](https://static.airtable.com/blocks/images/todo-6-strikethrough.gif)

Perfect!

### Making it interactive

If we modify the checkbox values in the table, we can watch the tasks in our
extension update in real time. Before we can call this extension "complete", though, we should allow the user to toggle that state from the task list itself.

We need to use the table method `updateRecordAsync` to modify the record. While
we could give each `Task` component a reference to the whole table, this will
make it harder for others to predict the extent of the `Task` component's
behavior. Instead, we'll make a function that can toggle whether a task is
complete or not and pass that function to the `Task` component.

```diff
import {
    FieldPickerSynced,
    initializeBlock,
    useBase,
    useRecords,
    useGlobalConfig,
    expandRecord,
    TablePickerSynced,
    TextButton,
} from '@airtable/blocks/ui';
import {globalConfig} from '@airtable/blocks';
import React, {useState} from 'react';

function TodoExtension() {
    const base = useBase();

    const globalConfig = useGlobalConfig();
    const tableId = globalConfig.get('selectedTableId');
    const completedFieldId = globalConfig.get('completedFieldId');

    const table = base.getTableByIdIfExists(tableId);
    const completedField = table ? table.getFieldByIdIfExists(completedFieldId) : null;


+   const toggle = (record) => {
+       table.updateRecordAsync(
+           record, {[completedFieldId]: !record.getCellValue(completedFieldId)}
+       );
+   };
+
    const records = useRecords(table);

    const tasks = records && completedField ? records.map(record => (
-       <Task key={record.id} record={record} completedFieldId={completedFieldId}>
+       <Task
+           key={record.id}
+           record={record}
+           onToggle={toggle}
+           completedFieldId={completedFieldId}
+       />
    )) : null;

    return (
        <div>
            <TablePickerSynced globalConfigKey="selectedTableId" />
            <FieldPickerSynced table={table} globalConfigKey="completedFieldId" />
            {tasks}
        </div>
    );
}

function Task({record, completedFieldId}) { /* No change */ }

initializeBlock(() => <TodoExtension />);
```

Now, we can update the `Task` component to detect when the user interacts with
the task (e.g. by clicking on it or pressing the `Enter` key) and
invoking the new function.

```diff
-function Task({record, completedFieldId}) {
+function Task({record, completedFieldId, onToggle}) {
    const label = record.name || 'Unnamed record';

    return (
        <div
            style={{
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'space-between',
                fontSize: 18,
                padding: 12,
                borderBottom: '1px solid #ddd',
            }}
        >
+           <TextButton
+               variant="dark"
+               size="xlarge"
+               onClick={() => {
+                   onToggle(record);
+               }}
+           >
                {record.getCellValue(completedFieldId) ? <s>{label}</s> : label}
+           </TextButton>
            <TextButton
                icon="expand"
                aria-label="Expand record"
                variant="dark"
                onClick={() => {
                    expandRecord(record);
                }}
            />
        </div>
    );
}
```

With that change in place, users can modify the records in the table, tracking
their progress as they accomplish each task. For more details about modifying
data in a base, check out [Write back to
Airtable](https://airtable.com/developers/extensions/guides/write-back-to-airtable.md).

## The end!

We covered a lot of ground, kudos for making it to the end!

Here's a quick recap of the parts of the SDK we used. You can click the links to read more in-depth
documentation about each one:

-   Part 1

    -   [useBase](https://airtable.com/developers/extensions/api/ui/hooks/usebase.md) hook to get the
        [base](https://airtable.com/developers/extensions/api/models/base.md) object and subscribe
        to schema changes.
    -   [base.getTableByName()](https://airtable.com/developers/extensions/api/models/base.md#getTableByName) to get a [table](https://airtable.com/developers/extensions/api/models/table.md) object.
    -   [table.selectRecords()](https://airtable.com/developers/extensions/api/models/table.md#selectRecords),
        [useRecords](https://airtable.com/developers/extensions/api/ui/hooks/userecords.md) hook, and
        [record.name](https://airtable.com/developers/extensions/api/models/record.md#name) to read the records in a table.
    -   [expandRecord()](https://airtable.com/developers/extensions/api/ui/utils/expandrecord.md) to expand records in Airtable.
    -   [TextButton](https://airtable.com/developers/extensions/api/ui/components/textbutton.md) to display a recognizable target for the user to click

-   Part 2
    -   [base.getTableByNameIfExists()](https://airtable.com/developers/extensions/api/models/base.md#getTableByNameIfExists) and [base.getTableByIdIfExists()](https://airtable.com/developers/extensions/api/models/base.md#getTableByIdIfExists)
    -   [TablePicker](https://airtable.com/developers/extensions/api/ui/components/tablepicker.md)
        component.
-   Part 3
    -   [globalConfig](https://airtable.com/developers/extensions/api/models/globalconfig.md) to store
        extension configuration.
    -   [useGlobalConfig](https://airtable.com/developers/extensions/api/ui/hooks/useglobalconfig.md)
        hook to watch changes to globalConfig.
    -   [TablePickerSynced](https://airtable.com/developers/extensions/api/ui/components/tablepicker/synced.md)
        component.
-   Part 4
    -   [FieldPickerSynced](https://airtable.com/developers/extensions/api/ui/components/fieldpickersynced.md) component.
    -   [table.updateRecordAsync()](https://airtable.com/developers/extensions/api/models/table.md#updateRecordsAsync) to
        modify cells of a given [record](https://airtable.com/developers/extensions/api/models/record.md)

## Extra credit

### Only showing records from a view

Right now, our extension shows all the records in our Tasks table. It might be more useful to have it
only show records from a specific
[view](https://support.airtable.com/docs/getting-started-with-airtable-views#what-is-a-view). Then
we can filter the view to control which records we see in the extension. For example, we could create a
filter to only show tasks that aren't done yet.

It's easy to switch from showing records in a table to showing records in a view:

```diff
import {
    FieldPickerSynced,
    initializeBlock,
    useBase,
    useRecords,
    useGlobalConfig,
    expandRecord,
    TablePickerSynced,
    TextButton,
+   ViewPickerSynced,
} from '@airtable/blocks/ui';
import {globalConfig, models} from '@airtable/blocks';
import React, {useState} from 'react';

function getCheckboxField(table, fieldId) { /* No changes */}

function TodoExtension() {
    const base = useBase();

    const globalConfig = useGlobalConfig();
    const tableId = globalConfig.get('selectedTableId');
+   const viewId = globalConfig.get('selectedViewId');
    const completedFieldId = globalConfig.get('completedFieldId');

    const table = base.getTableByIdIfExists(tableId);
+   const view = table ? table.getViewByIdIfExists(viewId) : null;
    const completedField = table ? table.getFieldByIdIfExists(completedFieldId) : null;

-   const records = useRecords(table);
+   const records = useRecords(view);

    const tasks = records && completedField ? records.map(record => (
        <Task
            key={record.id}
            record={record}
            onToggle={toggle}
            completedFieldId={completedFieldId}
        />
    )) : null;

    return (
        <div>
            <TablePickerSynced globalConfigKey="selectedTableId" />
+           <ViewPickerSynced table={table} globalConfigKey="selectedViewId" />
            <FieldPickerSynced table={table} globalConfigKey="completedFieldId" />
            {tasks}
        </div>
    );
}

function Task({record, doneField}) { /* No changes */ }

initializeBlock(() => <TodoExtension />);
```

Just like how we get the `Table` object by using `base.getTableByIdIfExists`, we get the `View`
object by using `table.getViewByIdIfExists`.

You can also pass `View` to `useRecords` - the `records` returned will only contain the records
that are visible in that view.

That's it! Try adding filters to the selected view. The records in the extension will automatically get
filtered out.
