Skip to main content

React

So far, these docs have discussed Reboot as a cloud-native backend. Reboot also generates code that allows web or mobile React components to easily query and mutate Reboot state.

Overview

Consider hello.proto with one state data type, Hello, one reader, and one writer method.

message Hello {
option (rbt.v1alpha1.state) = {
};
repeated string messages = 1;
}

service HelloInterface {
// Returns the current list of recorded messages.
rpc Messages(MessagesRequest) returns (MessagesResponse) {
option (rbt.v1alpha1.method).reader = {
};
}

// Adds a new message to the list of recorded messages.
rpc Send(SendRequest) returns (SendResponse) {
option (rbt.v1alpha1.method).writer = {
};
}
}

message MessagesRequest {}

message MessagesResponse {
repeated string messages = 1;
}

message SendRequest {
string message = 1; // E.g. "Hello, World".
}

message SendResponse {}

The rest of this document will discuss the React code that is generated from from this .proto file.

Installation

To access Reboot from your React app, add @reboot-dev/reboot-react to your package.json.

npm install -S @reboot-dev/reboot-react
info

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 protoc, add the --react flags to .rbtrc. For example:

protoc --react=web/src/api

This flag specifies where to put the generated React code. It often makes sense to have these files written inside the root directory of your React app, as shown above, because many React frameworks prevent referencing files outside the root directory.

Setup

All calls to Reboot generated code must occur within a Reboot context. This context is provided by a RebootClientProvider. It takes a RebootClient as its client prop and defines the endpoint the generated React code will use to connect to the Reboot backend.

  <React.StrictMode>
<RebootClientProvider client={client}>
<App />
</RebootClientProvider>
</React.StrictMode>
);

All generated Reboot React Custom Hooks can now be used inside <App /> or any of App's children.

tip

Use a TLS endpoint like https://dev.localhost.direct to take advantage of multiplexing, a requirement for any Reboot React app with more than 5 concurrent connections.

In practice, it is best to use an environment variable for your API endpoint, the argument passed to RebootClient's constructor.

For example:

root.render(
<React.StrictMode>

How environment variables are managed in your codebase depends on the React framework or library you use.

info

Reading state

You can call a reader reactively very simply:

const { useMessages, send } = useHello({ id: STATE_MACHINE_ID });
const { response } = useMessages();

Let's break this down.

useHello is a generated React custom hook that provides access to all of the methods defined on the Hello 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 Hello 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 } = useHello({ 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 in the .proto as Send using the lower camel case name send:

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 (they are mutually exclusive).

info

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 in hello.proto 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.

info

The aborted returned is not an exception and the generated React code does not throw.

Vanilla JavaScript

Reboot generates typed code from the data types outlined in your protobuf file. These types are incredibly useful but they aren't strictly necessary in a JavaScript environment. You can use Reboot in a vanilla JS environment by taking the extra step of compiling the generated TypeScript to JavaScript.

While vanilla JS practitioners might find this extra step burdensome, it can be used as an opportunity to target a specific version of JavaScript.

To make sure this happens on every change, use npx tsc with the --watch flag enabled or use the --tsc flag in your .rbtrc and a tsconfig.json correctly configured for the web.

Programming model

Reboot React provides conveniences beyond querying and mutating states, chief among them:

  1. Guarantee the local ordering of mutations.
  2. 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 for any error that is retryable, like a temporary network outage. These retries are idempotent, so this is perfectly safe to do.

We know an error is unretryable if it originates from the application. All other errors are retried with an exponential backoff.