Basics
Reactlit works by defining an application script. This script procedurally builds the UI, inserting elements into the DOM as needed. Similar to how Streamlit works, every state change reruns the script which in turn re-renders the UI. Reactlit employs a few tricks to try to minimize re-renders so that you should not see any flickering or lose focus state.
Primitives
Reactlit core provides the following set of primitives to use in your application script.
Display
The display
function is used to render a React node.
display(<div>Hello World</div>);display(`Hey there`);display( <div> <Spinner /> Loading... </div>);
It can optionally take an first argument string to be an id for the display. This allows later code to update the display.
display('loading-data', <div>Spinner /> Loading...</div>);const data = await fetchData();display('loading-data', <div>Data loaded!</div>);// or alternatively, this to clear the prior displaydisplay('loading-data', undefined);
Set State
The set
function is used to set state. Most of the time you will not need to
use it directly, instead you will use the view
function below, but it is necessary on occasion.
Additionally the raw state
object is also provided as part of the context.
set('mystate', 'Hello World');const mystate = state.mystate;
View
The view
function is used to render an input view which has state and to return that state.
Think of it as a special form of display
with a return value.
The first argument is the key of the view. This determines which state key the value will be stored in.
The second argument is the view definition. Some built-in view definitions are provided in the add-on packages like
@reactlit/vanilla
and @reactlit/radix
. Or you can define your own as described in the Defining Views guide.
The follow examples assume you have imported the Inputs
object from the @reactlit/radix
package.
const name = view( 'name', Inputs.Text({ label: 'Name', placeholder: 'Enter your name' }));const color = view( 'color', Inputs.Select(['red', 'green', 'blue'], { label: 'Pick a color' }));
display( <div> {name} picked {color} </div>);
Views are type-safe and the types are determined by the view definition itself.
Transform views
Some views define a getReturnValue
function which transforms the value returned by the view.
Transforms allow the state you want to store (what you would get back from the get
function) to be different
from the value that is returned to work with. This is especially useful for elements like tables where
the selected state should be some kind of a row id, but the view should return the full row data.
Assuming you have a variable users
which is an array of user objects, you can define a view like this:
const selectedUser = view( 'user', Inputs.Table(users, { getRowId: (row) => row.userId }));const selectedUserId = state.user;display( <div> Hello {selectedUser.name}. Your id is {selectedUserId} </div>);
Changed
Sometimes you need to trigger side-effects when state changes. In React, we have useEffect
to handle this.
The equivalent in Reactlit is a changed
function which tells you if the provided state keys have changed since the
last run of the script.
We use the changed
function here to reset the state of the email input when the selected user changes.
const selectedUser = view( 'user', Inputs.Table(users, { getRowId: (row) => row.userId }));if (changed('user')) { set('email', selectedUser.email);}const email = view('email', Inputs.Text({ label: 'Update your Email' }));display( <button onClick={async () => setUserEmail(selectedUser.userId, email)}> Update </button>);
Trigger
Sometimes you need to trigger the script to rerun even if state has not changed. Typically you do this when you apply mutations and want to refetch data.
display( <button onClick={async () => { await setUserEmail(selectedUser.userId, email); // rerun the script so the users table will re-fetch and be updated trigger(); }} > Update </button>);
Putting it all together
The following example application puts together all of these primitives so you can see how they fit together.
import { Button } from '@radix-ui/themes';import { type ReactlitContext } from '@reactlit/core';import { Inputs, Label } from '@reactlit/radix';import { ContactMockApi as api } from '../mocks/contacts';
export async function ContactListApp(app: ReactlitContext) { const contacts = await api.getContacts(); 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); } // the built-in FormView allows you to group inputs together const updates = { name: app.view('name', Label('Name'), Inputs.Text()), email: app.view('email', Label('Email'), Inputs.Text()), }; app.display( <Button onClick={async () => { await api.updateContact(selectedContact.id, updates); app.trigger(); }} > Update </Button> );}
import { Theme } from '@radix-ui/themes';import { Reactlit } from '@reactlit/core';import '@radix-ui/themes/styles.css';import { ContactListApp } from './apps/contact-list';
export default function ContactList() { return ( <Theme className="not-content" data-is-root-theme={false}> <Reactlit>{ContactListApp}</Reactlit> </Theme> );}
// This is mocking a backend API for demo purposes
import { wait } from '../utils/wait';
export type Contact = { id: string; name: string; email: string;};
// we add a delay to these to simulate a network requestexport class ContactsMockService { constructor( private contacts: Contact[], private readonly delay: number = 0 ) {}
async getContacts() { await wait(this.delay); return this.contacts; }
async addContact(contact?: Partial<Omit<Contact, 'id'>>) { await wait(this.delay); const newContact = { name: `New Contact ${this.contacts.length + 1}`, email: `contact${this.contacts.length + 1}@example.com`, ...(contact ?? {}), id: `contact-${this.contacts.length + 1}`, }; this.contacts = [...this.contacts, newContact]; return newContact; }
async updateContact(id: string, contact: Partial<Contact>) { await wait(this.delay); const index = this.contacts.findIndex((c) => c.id === id); if (index === -1) { throw new Error('Contact not found'); } this.contacts = [ ...this.contacts.slice(0, index), { ...this.contacts[index], ...contact }, ...this.contacts.slice(index + 1), ]; return this.contacts[index]; }}
export const ContactMockApi = new ContactsMockService([], 0);
See it live: