From React
Reboot generates code that allows web or mobile React components to easily query and mutate Reboot data types.
Overview
Consider a ChatRoom API definition with one data type, ChatRoom, one
reader, and one
writer method.
- Zod
- Pydantic
import { reader, writer } from "@reboot-dev/reboot-api";
import { z } from "zod/v4";
export const ChatRoom = {
state: {
messages: z.array(z.string()).default([]).meta({ tag: 1 }),
},
methods: {
// Returns the current list of recorded messages.
messages: reader({
request: {},
response: {
messages: z.array(z.string()).meta({ tag: 1 }),
},
}),
// Adds a new message to the list of recorded messages.
send: writer({
request: {
message: z.string().meta({ tag: 1 }), // E.g. "Hello, World".
},
response: z.void(),
}),
},
};
export const api = {
ChatRoom,
};
from reboot.api import API, Field, Methods, Model, Reader, Type, Writer
from typing import Optional
class ChatRoomState(Model):
messages: Optional[list[str]] = Field(tag=1)
class MessagesResponse(Model):
messages: list[str] = Field(tag=1)
class SendRequest(Model):
message: str = Field(tag=1) # E.g. "Hello, World".
ChatRoomMethods = Methods(
# Returns the current list of recorded messages.
messages=Reader(
request=None,
response=MessagesResponse,
description="Every message posted to the room so far.",
mcp=None,
),
# Adds a new message to the list of recorded messages.
send=Writer(
request=SendRequest,
response=None,
description="Post one message to the room.",
mcp=None,
),
)
api = API(
ChatRoom=Type(
state=ChatRoomState,
methods=ChatRoomMethods,
),
)
The rest of this document will discuss the React code that is generated from your API definition.
Installation
To access Reboot from your React app, add @reboot-dev/reboot-react
to your package.json.
npm install -S @reboot-dev/reboot-react
To see the complete code referenced in this page, go to the 'reboot-hello' example.
To have the rbt command line utility generate React code when running
rbt generate, add the --react flag to .rbtrc. For example:
generate --react=frontend/api
This flag specifies where to put the generated React code. The
examples generate into frontend/api/, a sibling of the app in
frontend/web/, so that every frontend of an application (web,
mobile, MCP UIs) shares one generated client. Some React frameworks
prevent referencing files outside the app's root directory
by default; for Vite, allow it with server.fs.allow: [".."] in
vite.config.ts.
Setup
All calls to Reboot generated code must occur within a Reboot context. This
context is provided by a RebootClientProvider. It takes a
url prop that defines the endpoint the
generated React code will use to connect to the Reboot backend.
<RebootClientProvider url={"http://localhost:9991"}>
<App />
</RebootClientProvider>
All generated Reboot React Custom Hooks can now be used inside <App /> or
any of App's children.
Use a TLS endpoint so the browser can use HTTP/2. Over plain HTTP,
every reactive reader holds its own WebSocket, and browsers allow
only around 200 of those per page (255 in Chrome). See
the rbt CLI
for local certificates.
In practice, it is best to use an environment variable for your API endpoint.
For example:
(import.meta.env.VITE_REBOOT_URL as string) || "http://localhost:9991";
How environment variables are managed in your codebase depends on the React framework or library you use.
Reboot generates TypeScript code. Learn how to use Reboot in a vanilla JS environment.
Signing users in
When your application configures oauth=, three more
hooks come into play:
useSignIn()anduseSignOut()from@reboot-dev/reboot-react.- The generated
use<User>()hook for yourUsertype, which resolves the signed-in user with no ID passed.
const { user, isLoading } = useUser();
const signIn = useSignIn();
if (isLoading) return <p>Checking session…</p>;
if (user === undefined) return <button onClick={() => signIn()}>Sign in</button>;
See Web apps for the browser flow, and React Native apps for the native one.
In MCP UIs
The same generated hooks work unchanged inside an
MCP client, where a
UI method renders your component. A few
things differ:
- No
urlprop needed. Inside an MCP client,RebootClientProviderautomatically detects the environment. You do not pass aurl:
<RebootClientProvider>
<ClickerApp />
</RebootClientProvider>
- Automatic ID resolution. For
Usermethods, the hook resolves the state ID from the authenticated user automatically. For other types (likeCounter), the ID is provided by the framework when the UI is opened. Called without anid, the hook returns{ counter, isLoading }, wherecounterisundefineduntil the ID resolves, so check both before using the handle (seeUImethods for a complete component):
export const ClickerApp: FC = () => {
const { counter, isLoading } = useCounter();
if (isLoading) {
return <div>loading...</div>;
}
// `isLoading` is checked first: while it is true, `counter` is
// still `undefined`. Reaching this check with `undefined` therefore
// means resolution finished and there is genuinely no default
// Counter id.
if (counter === undefined) {
console.error("No default Counter id was available; cannot render.");
return <div>An error occurred, sorry about that!</div>;
}
return <Clicker counter={counter} />;
};
const Clicker: FC<{ counter: UseCounterApi }> = ({ counter }) => {
const [isPending, setIsPending] = useState(false);
const { response, isLoading } = counter.useGet();
The resolved counter handle exposes the same API as
useCounter({ id }) — e.g., counter.useGet() above, or calling
a mutator with counter.increment({ amount: 1 }).
UImethod props. ForUImethods with arequesttype, the AI-provided fields are passed to the React component as props:
export const DashboardApp: FC<DashboardConfig> = ({ personalizedMessage }) => {
const { counter, isLoading } = useCounter();
The DashboardConfig props type is imported from the same
generated file as useCounter.
Reading state
You can call a reader reactively very simply:
const { useMessages, send } = useChatRoom({ id: STATE_MACHINE_ID });
const { response } = useMessages();
Let's break this down.
useChatRoom is a generated React custom hook that provides access to all of the methods
defined on the ChatRoom state data type. The id that is passed is the ID that uniquely identifies your state.
For every reader method, a
React custom hook is generated, e.g., useMessages. Any time the ChatRoom state
with the given id changes, response is updated and the component re-renders.
In this specific case, useMessages can be called with no arguments because
Messages takes an empty request.
Mutating state
Mutators such as writer
and transaction methods
are both callable from React.
const { useMessages, send } = useChatRoom({ id: STATE_MACHINE_ID });
Reboot methods are accessed by their name in lower camel case, e.g.,
deleteAllMessages.
This line calls the
writer method declared as send in the API
definition, using its lower camel case name:
const { aborted } = await send({ message: message });
In the example above, send is passed a partial
but it will also happily take a SendRequest as an argument instead.
Optimistic updates
To provide a snappy user experience, it is common to optimistically render the result of a mutation before the result has been committed.
Reboot attaches all in-flight mutations to a .pending property of every mutator to
facilitate this, for example:
{send.pending.map(({ request: { message }, isLoading }) => (
<PendingMessage text={message} isLoading={isLoading} key={message} />
))}
In this example, all in-flight mutations are rendered using a PendingMessage
wrapper that gives the user an indication that their message has been sent but
not yet received.
You can be sure that a mutation is either pending, has been applied, or has failed (these are mutually exclusive).
Assume there is a mutation denoted 'mutation-xy'. As soon as
response sees a version of state that has committed 'mutation-xy',
'mutation-xy' is atomically removed from .pending. No need to worry about
data races!
Errors
Every call to a mutator returns both a response and an aborted; successful calls will ensure aborted is undefined.
Learn more about Reboot errors and error types.
const { aborted } = await send({ message: message });
if (aborted !== undefined) {
console.warn(aborted.error.getType());
console.warn(aborted.message);
}
In this case, because the send method does not define any specific
error types, the only error that can be returned in the aborted
object will be Reboot system errors such as StateAlreadyConstructed.
The aborted returned is not an exception and the generated React code does not throw.
Caching reads
Readers can be cached and persisted in the frontend for longer durations. In case the frontend is disconnected from the network or the Reboot backend when loading, the response for a reader will be read from an IndexedDB or as a fallback from the localStorage of the application's browser. This enables PWAs to start up in disconnected mode by showing the most recent persisted reader response and keeps your application in a readable state if the backend is not available.
Enable reader response caching by setting offlineCacheEnabled to true
at the RebootClientProvider. Cache entries have no TTL set, that means
they won't expire as long as the used OfflineCacheStorageType can hold
those entries, which means usually forever.
<RebootClientProvider url={"http://localhost:9991"} offlineCacheEnabled>
<App />
</RebootClientProvider>
To determine the auto-detected OfflineCacheStorageType, you can check
on offlineCacheStorageType, which is exported by your generated React
hooks file. Possible values are IndexedDB, LocalStorage and
InMemory.
InMemory is ephemeral when the browser tab closes and is only used, if
the other types are not available.
Caching is disabled by default, as data may contain sensitive
information and the browser's storage is potentially vulnerable to XSS
attacks. You explicitly need to enable caching at the
RebootClientProvider.
Next.js and Server Components
Using Reboot-generated TypeScript on the server and Reboot-generated React on the client allows you to create high-performance applications that show your users all the data they need on first-paint, while also allowing for all of the amazing reactive features that come with the Reboot React client-side library.
The Reboot React library is composed of client-side custom hooks that provide reactivity whenever Reboot state changes occur. In the case that you want to do something server-side with Reboot and React before returning to the client, use the Reboot-generated server-side TypeScript.
For instance, you might want to make a call to Reboot state on the server first, render HTML with data included and then send it to the client. This allows you to send a non-interactive and non-reactive page to the user that then becomes interactive and reactive upon hydration.
This can be achieved by combining Reboot TypeScript backend-generated code and Next.js Server Components.
To see the complete example, check out the Reboot Counter example repo.
On the server, the code that is typically generated for TypeScript
backends can be used to fetch initial data. In this case, we call
.count(), a non-reactive, unary
reader method, to supply
the initial value.
Importantly, we also need to construct an
ExternalContext to make
this call safely.
export default async function Home() {
const context = new ExternalContext({
name: "react server context",
url: process.env.NEXT_PUBLIC_ENDPOINT,
});
const counts = await Promise.all(
COUNTER_IDS.map(async (id: string) => Counter.ref(id).count(context))
);
return COUNTER_IDS.map((id, index) => (
<TakeableCounter id={id} key={id} initialCount={counts[index].count} />
));
}
The main difference from a client-only component is that TakeableCounter now takes
an initialCount prop that the server is expected to provide.
The client-side call is completed as normal, but the data can be rendered immediately because it is passed into the component as a prop.
const TakeableCounter: React.FC<{ id: string; initialCount: number }> = ({
id,
initialCount,
}) => {
const { useCount, increment, take } = useCounter({ id });
const { response } = useCount();
Now it is easy to render either the initialCount that was passed in initially or the
current reactive value of the response.count object.
count={response ? response.count : initialCount}
To learn more about React Server Components, refer to the React Server Components docs or, for information specific to Next.js, visit Next's page on Server Components.
Vanilla JavaScript
Reboot automatically transpiles TypeScript into JavaScript by way of esbuild.
If you wish to use a tool other than esbuild for transpilation, you can pass a
command to the --transpile flag in your .rbtrc.
--transpile=npx tsc
Programming model
Reboot React provides conveniences beyond querying and mutating states, chief among them:
- Guaranteed local ordering of mutations.
- Automatic retries.
Ordering
The order in which the mutations are called on the client is the order in which they will be executed on the server. From the client's perspective, a 'happens-before' relationship is maintained.
Assume a client performs the following mutations A -> B -> C.
Another client might be performing D -> E simultaneously.
A possible global ordering might be A -> B -> D -> E -> C or
D -> A -> E -> B -> C, but never D -> B -> E -> A -> C.
State mutations that have occurred while a client's mutations are in-flight won't be shown to the client until those mutations have committed or result in an error.
Automatic retries
All calls are retried if they result in any retryable error, such as a temporary network outage. These retries are idempotent, so this is perfectly safe to do.
An error is considered unretryable if it originates from the application. All other errors are retried with an exponential backoff.