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.
import { Button } from '@radix-ui/themes';import { type ReactlitContext } from '@reactlit/core';import { Inputs, Label } from '@reactlit/radix';import { TopRightLoader } from '../components/loader';import { ContactsMockService } from '../mocks/contacts';
// add a delay to the mock APIconst 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('name', selectedContact.name); app.set('email', selectedContact.email); } const updates = { name: app.view('name', Label('Name'), Inputs.Text()), email: app.view('email', Label('Email'), Inputs.Text()), }; // 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.
22 collapsed lines
import { Button, Theme } from '@radix-ui/themes';import '@radix-ui/themes/styles.css';import { DataFetchingPlugin, useReactlit } from '@reactlit/core';import { Inputs, Label } 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 APIconst api = new ContactsMockService([], 1000);
const ContactListApp = () => { const Reactlit = useReactlit(DataFetchingPlugin); return ( <Reactlit> {async (app) => { // // 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> ); const selectedContact = app.view( 'selectedContact', Inputs.Table(contacts, { getRowId: (contact) => contact.id, columns: ['name', 'email'], })16 collapsed lines
); if (!selectedContact) return; app.display( <h3 style={{ paddingTop: '1rem' }}>Selected Contact Details</h3> ); if (app.changed('selectedContact')) { app.set('name', selectedContact.name); app.set('email', selectedContact.email); } const updates = { name: app.view('name', Label('Name'), Inputs.Text()), email: app.view('email', Label('Email'), Inputs.Text()), }; 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> ); }} </Reactlit> );};
The following app behaves much more smoothly even with a slower API.