Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

React

Let's start with the new part, and also typically the shorter part – implementing the capabilities.

Capability implementation

This is what Weather's core.ts looks like

export class Core {
  core: CoreFFI;
  callback: Dispatch<SetStateAction<ViewModel>>;

  constructor(callback: Dispatch<SetStateAction<ViewModel>>) {
    this.callback = callback;
    this.core = new CoreFFI();
  }

  update(event: Event) {
    const serializer = new BincodeSerializer();
    event.serialize(serializer);

    const effects = this.core.update(serializer.getBytes());

    const requests = deserializeRequests(effects);
    for (const { id, effect } of requests) {
      this.resolve(id, effect);
    }
  }
  // ...
}

It's slightly more complicated, but broadly the same as the Counter's core. We wrap the CoreFFI (loaded via WASM) and hold on to a React setState callback, which we use to update the view model whenever the core asks us to render.

We've truncated the resolve method, because it's fairly long, but the basic structure is this:

  async resolve(id: number, effect: Effect) {
    switch (effect.constructor) {
      case EffectVariantRender:
        // ...

      case EffectVariantHttp:
        // ...

      case EffectVariantKeyValue:
        // ...

      case EffectVariantLocation:
        // ...
    }
  }

We get a Request, and do a switch on what the requested effect's constructor is to determine the type. In TypeScript we use instanceof-style constructor checks, so we can also cast and destructure the operation requested.

We can have a look at what the HTTP branch does:

      case EffectVariantHttp: {
        const request = (effect as EffectVariantHttp).value;
        const response = await http.request(request);
        this.respond(id, response);
        break;
      }

This delegates to http.request(), which does the actual work, and then calls this.respond() with the result:

  respond(id: number, response: Response) {
    const serializer = new BincodeSerializer();
    response.serialize(serializer);

    const effects = this.core.resolve(id, serializer.getBytes());

    const requests = deserializeRequests(effects);
    for (const { id, effect } of requests) {
      this.resolve(id, effect);
    }
  }

We use async/await to run the HTTP request, then take the response, serialize it and pass it to core.resolve via respond, which returns more effect requests. This is perhaps unexpected, but it's the direct consequence of the Commands async nature. There can easily be a command which does something along the lines of:

Command::new(|ctx| {
    let http_req = Http::get(url).expect_json<Counter>().build().into_future(ctx);
    let resp = http_req.await; // effect 1

    let counter = resp.map(|result| match result {
        Ok(mut response) => match response.take_body() {
            Some(counter) => {
                Ok(results)
            }
            None => Err(ApiError::ParseError),
        },
        Err(_) => Err(ApiError::NetworkError),
    });

    let _ = KeyValue::set(COUNTER, counter).into_future(ctx).await // effect 2

    // ...

    ctx.send_event(Event::Done);
})

Once we resolve the http request at the .await point marked "effect 1", this future can proceed and make a KeyValue request at the "effect 2" .await point. So on the shell end, we need to be able to respond appropriately.

What we do is loop through those effect requests (there could easily be multiple requests at once), go through them and recurse—call resolve again to handle each one.

Just for completeness, this is what http.ts looks like:

import type { HttpRequest, HttpResult } from "shared_types/app";
import {
  HttpResponse,
  HttpHeader,
  HttpResultVariantOk,
} from "shared_types/app";

export async function request({
  url,
  method,
  headers,
}: HttpRequest): Promise<HttpResult> {
  const request = new Request(url, {
    method,
    headers: headers.map((header) => [header.name, header.value]),
  });

  const response = await fetch(request);

  const responseHeaders = Array.from(
    response.headers.entries(),
    ([name, value]) => new HttpHeader(name, value),
  );

  const body = await response.arrayBuffer();

  return new HttpResultVariantOk(
    new HttpResponse(response.status, responseHeaders, new Uint8Array(body)),
  );
}

Not that interesting, it's a wrapper around the browser's native fetch API which takes and returns the generated HttpRequest and HttpResponse, originally defined in Rust by crux_http.

The pattern repeats similarly for key-value store and the location capability.

User interface and navigation

It's worth looking at how Weather handles the Workflow navigation in React.

As in the Counter example, the Weather's core holds a ViewModel which we store in React state via useState, so the component re-renders whenever the core asks us to.

Here's the root component:

const Home: NextPage = () => {
  const [view, setView] = useState(
    new ViewModel(new WorkflowViewModelVariantHome(null!, [])),
  );
  const core: React.RefObject<Core | null> = useRef(null);

  const initialized = useRef(false);
  useEffect(
    () => {
      if (!initialized.current) {
        initialized.current = true;

        init_core().then(() => {
          if (core.current === null) {
            core.current = new Core(setView);
          }
          core.current?.update(
            new EventVariantHome(new WeatherEventVariantShow()),
          );
        });
      }
    },
     
    /*once*/ [],
  );

  const workflow = view.workflow;

  return (
    <main>
      <section className="section has-text-centered">
        <p className="title">Crux Weather Example</p>
        <p className="is-size-5">Rust Core, TypeScript Shell (Next.js)</p>
      </section>
      <section className="container">
        {workflow instanceof WorkflowViewModelVariantHome && (
          <HomeView
            weatherData={workflow.weather_data}
            favorites={workflow.favorites}
            core={core}
          />
        )}
        {workflow instanceof WorkflowViewModelVariantFavorites && (
          <FavoritesView
            favorites={workflow.favorites}
            deleteConfirmation={workflow.delete_confirmation}
            core={core}
          />
        )}
        {workflow instanceof WorkflowViewModelVariantAddFavorite && (
          <AddFavoriteView searchResults={workflow.search_results} core={core} />
        )}
      </section>
    </main>
  );
};

We initialize the WASM core inside a useEffect that runs once, create a Core instance with the setView callback, and immediately dispatch the initial Show event to kick things off.

Thanks to the declarative nature of React, we can show the view we need to, depending on the workflow, using instanceof checks on the workflow variant. Each branch renders the appropriate component and passes the core ref down.

We could do this differently—core could stay in the root component and we could pass an update callback via React context, and just the appropriate section of the view model to each component. You could also use React Router for navigation. It's up to you how you want to go about it.

Let's look at the HomeView as well, just to complete the picture:

function HomeView({
  weatherData,
  favorites,
  core,
}: {
  weatherData: unknown;
  favorites: unknown[];
  core: React.RefObject<Core | null>;
}) {
   
  const wd = weatherData as any;
  const hasData = wd && wd.cod == 200;
   
  const favs = favorites as any[];

  return (
    <>
      <div className="box">
        {hasData ? (
          <div className="has-text-centered">
            <h2 className="title is-4">{wd.name}</h2>
            <p className="is-size-1 has-text-weight-bold">
              {wd.main.temp.toFixed(1)}&deg;
            </p>
            {wd.weather?.[0] && (
              <p className="is-size-5">{wd.weather[0].description}</p>
            )}
            <div className="columns is-multiline is-centered mt-4">
              <div className="column is-one-third">
                <p className="heading">Feels Like</p>
                <p>{wd.main.feels_like.toFixed(1)}&deg;</p>
              </div>
              <div className="column is-one-third">
                <p className="heading">Humidity</p>
                <p>{Number(wd.main.humidity)}%</p>
              </div>
              <div className="column is-one-third">
                <p className="heading">Wind</p>
                <p>{wd.wind.speed.toFixed(1)} m/s</p>
              </div>
              <div className="column is-one-third">
                <p className="heading">Pressure</p>
                <p>{Number(wd.main.pressure)} hPa</p>
              </div>
              <div className="column is-one-third">
                <p className="heading">Clouds</p>
                <p>{Number(wd.clouds.all)}%</p>
              </div>
              <div className="column is-one-third">
                <p className="heading">Visibility</p>
                <p>{Math.floor(Number(wd.visibility) / 1000)} km</p>
              </div>
            </div>
          </div>
        ) : (
          <p className="has-text-centered">Loading weather data...</p>
        )}
      </div>
      {favs.length > 0 && (
        <div className="box">
          <h3 className="title is-5">Favorites</h3>
          {favs.map((fav, i) => {
            const w = fav.current;
            return (
              <div key={i} className="box">
                <strong>{fav.name}</strong>
                {w ? (
                  <div className="columns is-multiline mt-2">
                    <div className="column is-one-third">
                      <p className="is-size-3 has-text-weight-bold">
                        {w.main.temp.toFixed(1)}&deg;
                      </p>
                    </div>
                    <div className="column is-one-third">
                      {w.weather?.[0] && <p>{w.weather[0].description}</p>}
                    </div>
                    <div className="column is-one-third">
                      <p>Humidity: {Number(w.main.humidity)}%</p>
                    </div>
                  </div>
                ) : (
                  <p className="has-text-grey">Loading...</p>
                )}
              </div>
            );
          })}
        </div>
      )}
      <div className="buttons is-centered mt-4">
        <button
          className="button is-info"
          onClick={() =>
            core.current?.update(
              new EventVariantNavigate(
                new WorkflowVariantFavorites(new FavoritesStateVariantIdle()),
              ),
            )
          }
        >
          Favorites
        </button>
      </div>
    </>
  );
}

It simply caters for the possible situations in the view model—checking whether cod === 200 to decide if weather data has loaded—draws the weather cards with a grid of details, and adds a "Favorites" button which when clicked calls core.current?.update with the TypeScript equivalent of the .navigate event we saw earlier in the core.

This is quite a simple navigation setup in that it is a static set of screens we're managing. Sometimes a more dynamic navigation is necessary, but React Router or similar libraries support quite complex scenarios in a declarative fashion, so the general principle of naively projecting the view model into the user interface broadly works even there.

There isn't much more to it, the rest of the app is rinse and repeat. It is relatively rare to implement a new capability, so most of the work is in finessing the user interface. Crux tends to work reasonably well with hot module reloading so you can typically avoid full page reloads for the inner development loop.

What's next

Congratulations! You know now all you will likely need to build Crux apps. The following parts of the book will cover advanced topics, other support platforms, and internals of Crux, should you be interested in how things work.

Happy building!