Skip to content

Data Fetching

Simple Fetching

Since Reactlit scripts are async functions, you can fetch data directly in your script. This works fine for simple use cases, but you should keep in mind that every time the script runs your fetch will be called. Generally, we recommend using a cache to store your data. To accomplish this, you can use solutions like Next.js request memoization or Tanstack Query.

display('loading-items', <div>Loading Items...</div>);
const data = await fetchItems();
display('loading-items', undefined);

This will display a loading state while the data is being fetched, but then clear it.

Let’s take a look at the example from the basics guide, and see how it would work with async fetching and some request latency.

apps/contact-list.tsx
import { Button } from '@radix-ui/themes';
import { FormInput, type ReactlitContext } from '@reactlit/core';
import { Inputs } from '@reactlit/radix';
import { TopRightLoader } from '../components/loader';
import { ContactsMockService } from '../mocks/contacts';
// add a delay to the mock API
const api = new ContactsMockService([], 500);
export async function ContactListApp(app: ReactlitContext) {
// wrap a loader around the contacts fetch
app.display('loading-items', <TopRightLoader text="Loading Contacts..." />);
const contacts = await api.getContacts();
app.display('loading-items', undefined);
app.display(
<Button
onClick={async () => {
const newContact = await api.addContact();
app.set('selectedContact', newContact.id);
}}
>
Add Contact
</Button>
);
const selectedContact = app.view(
'selectedContact',
Inputs.Table(contacts, {
getRowId: (contact) => contact.id,
columns: ['name', 'email'],
})
);
if (!selectedContact) return;
app.display(<h3 style={{ paddingTop: '1rem' }}>Selected Contact Details</h3>);
if (app.changed('selectedContact')) {
app.set('updates', selectedContact);
}
const updates = app.view(
'updates',
FormInput({
name: Inputs.Text({ label: 'Name' }),
email: Inputs.Text({ label: 'Email' }),
})
);
// if you wish, you can use an AsyncButton view for a button that
// has a loading state during async operations
app.view(
'updating',
Inputs.AsyncButton(
async () => {
await api.updateContact(selectedContact.id, updates);
app.trigger();
},
{
content: 'Update',
}
)
);
}

While this app works okay, the loader appears even as you type text into the inputs (see caching discussion above). In fact, any update triggers loading the data again. Furthermore, the app does not disable interaction, if you try to type while the data is loading, your entry might revert, resulting in a poor user experience. This effect is more significant if the fetching is slower. If your API is very fast you may not need to worry about these issues.

Next, we’ll explore using the data fetching plugin to solve these issues.

Data Fetching Plugin

The above fetching method works fine for simple use cases, but when you add a cache, you will need to coordinate invalidating the cache and triggering a re-render when the data changes. Additionally, the other components in your app have no way of responding to the loading state of the data and your app essentially pauses to wait for the data to load.

The Data Fetching Plugin offers solutions to these problems. It is built on top of Tanstack Query and provides a type-safe way to create a DataFetcher instance which can be used to get the current data synchronously, detect loading state, and update and invalidate the cache.

The following is an example of how you could use this plugin to improve the data fetching from above.

apps/contact-list.tsx
24 collapsed lines
import { Button, Theme } from '@radix-ui/themes';
import '@radix-ui/themes/styles.css';
import {
DataFetchingPlugin,
FormInput,
Reactlit,
type ReactlitPluginContext,
type StateBase,
} from '@reactlit/core';
import { Inputs } from '@reactlit/radix';
import { TopRightLoader } from './components/loader';
import { ContactsMockService } from './mocks/contacts';
export default function ContactList() {
return (
<Theme
style={{ position: 'relative' }}
className="not-content"
data-is-root-theme={false}
>
<ContactListApp />
</Theme>
);
}
// slow down the mock API to demo user experience with a slow API
const api = new ContactsMockService([], 1000);
const ContactListApp = () => (
// be sure to add the `as const` so the typing works correctly
<Reactlit plugins={[DataFetchingPlugin] as const}>
{ContactListAppScript}
</Reactlit>
);
const ContactListAppScript = async (
app: ReactlitPluginContext<StateBase, [typeof DataFetchingPlugin]>
) => {
//
// create a fetcher for the contacts with a cache key of ['contacts']
const contactsFetcher = app.fetcher(['contacts'], () => api.getContacts());
if (contactsFetcher.isFetching()) {
app.display('loader', <TopRightLoader text="Loading Contacts..." />);
}
// get the current contacts synchronously, back off to empty list if null
const contacts = contactsFetcher.get() ?? [];
app.display(
<Button
onClick={async () => {
// display another loader while the new contact is being added
app.display('loader', <TopRightLoader text="Adding new contact..." />);
const newContact = await api.addContact();
app.display('loader', undefined);
// trigger a re-fetch of the contacts
await contactsFetcher.refetch();
app.set('selectedContact', newContact.id);
}}
>
Add Contact
</Button>
);
19 collapsed lines
const selectedContact = app.view(
'selectedContact',
Inputs.Table(contacts, {
getRowId: (contact) => contact.id,
columns: ['name', 'email'],
})
);
if (!selectedContact) return;
app.display(<h3 style={{ paddingTop: '1rem' }}>Selected Contact Details</h3>);
if (app.changed('selectedContact')) {
app.set('updates', selectedContact);
}
const updates = app.view(
'updates',
FormInput({
name: Inputs.Text({ label: 'Name' }),
email: Inputs.Text({ label: 'Email' }),
})
);
app.display(
<Button
onClick={async () => {
app.display('loader', <TopRightLoader text="Updating contact..." />);
const updatedContact = await api.updateContact(
selectedContact.id,
updates
);
// update the contacts cache directly to avoid needing to refetch
// you can do this in-lieu of calling contactsFetcher.refetch() to avoid the
// extra network request
contactsFetcher.update((contacts) =>
contacts.map((contact) =>
contact.id === updatedContact.id ? updatedContact : contact
)
);
app.display('loader', undefined);
}}
// disable the button if the contacts are currently being fetched
disabled={contactsFetcher.isFetching()}
>
Update
</Button>
);
};

The following app behaves much more smoothly even with a slower API.