Rust-y parenting: WebAssembly 1 / 2

jcbellido August 25, 2021 [Code] #LillaOst #Rust

This article covers the evolution of LillaOst from an ad-hoc web solution using server side HTML generation into a Single-Page Application a-la React based on WebAssembly. And how the introduction of Yew results in a more scalable and easier to maintain project.

And best of all, you can test it yourself: WebAssembly LillaOst. It's free and private. And perhaps, if you find something weird, add a nice issue, pretty please?.

New LillaOst Home Page

How to test the app

This text will make way more sense if you check LillaOst for yourself. To make things as simple as possible LO is able to populate itself with mock data, you only need to provide a couple of fake names, in detail:

  1. Navigate to LillaOst.
  2. You should see a modal message asking you to Go to Settings.
  3. In the Tracked persons section add a couple names: Alice, Bob, Charlie.
  4. At the bottom of the page you'll see a button: Add rand. 6000, click it.
  5. In the middle of the page there's a button: Home, click it.
  6. Take a look around, perhaps check the navigation bar, summary, add a new event, you decide.
  7. When you're done, use the navigation to go back to Settings.
  8. Clean the used local storage by selecting: Purge all data.

And one last time: LO is completely private. No cookies, no weird tracking, no nothing. It's a client-side solution.

The model has grown

As sometimes happen with tools written under very specific production needs, LillaOst was quite specialized. It's evident that tracking a newly-born is not the same as tracking a toddler or an older kid. And perhaps somewhat comically, it took me around 6 months to notice that is also quite usual to have more than one kid to track. The first step was to expand LO to:

  1. Track more than one kid.
  2. Keep track of the solids (as in grams) consumed in a meal.
  3. Introduce more general events and notes, such as: bath, naps, medication, etc.

Frontend: Yew + WebAssembly

Why adopting yew?

The simplistic but functional server side HTML rendering that I was using in the first version of LillaOst (see handlebars) was becoming difficult to maintain. As an example, the introduction of a new button required:

  1. Open the .html template and add the HTML to render the button.
  2. In the <button .../> element, add a call to JS.
  3. Add the JS implementation of the button call.
  4. If needed, expand the server with the required new endpoints.

Any tiny change over the .html templates forced a server restart and the cascade of linked templates resulted on a byzantine structure.

In contrast, Yew offers two advantages out of the box:

  1. Components combine looks and behavior in a single package: changes are way simpler to track and do.
  2. No need to restart: trunk serve offers a quick iteration loop.

In their own words ==> It features a component-based framework which makes it easy to create interactive UIs.Developers who have experience with frameworks like React and Elm should feel quite at home when using Yew.

Yew is one of the recommended and reasonably mature front-end rust libraries for building web apps. If you've worked with React, and I have, it's a library that I can recommend.

Learning Yew

The project's web itself, Yew, lands directly on the documentation for it. It's a good starting point. But what really got me interested was the router example:

I was aiming for a Single-Page Application look and feel and this example was a great starting point. It uses the CSS framework bulma and since I know almost nothing about CSS frameworks, except, perhaps that there's something called BootStrap I simply went ahead with it. In general, browsing the examples is a good idea. Some examples, such as boids (source) / boids (live) are incredibly Flash-y.

WebAssembly build tools: Trunk

Yew's manual lists a couple options to build your WebAssembly. But after testing trunk I don't know why you would choose any other for a new project. It's also the solution used in the yew examples. As a build tool it's more than enough to construct LillaOst and I'm not even close to squeezing every feature from it. As an example my index.html looks like:

... scaffolding
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>LillaOst</title>
    <base data-trunk-public-url />
    <link data-trunk rel="icon" href="favicon.ico" />
    <link data-trunk rel="css" href="darkly-bulmaswatch.min.css" />
    <link rel="stylesheet" type="text/css" href="darkly-bulmaswatch.min.css" />
    <link data-trunk rel="sass" href="index.scss" />
</head>
<body>
    <link data-trunk rel="rust" href="Cargo.toml" data-bin="yew_lillaost" data-wasm-opt="z" />
</body>
... scaffolding
Trunk: Minimizing binary size

If you check the <body> section in the snippet above you'll notice the attribute: data-wasm-opt="z" that means optimize for minimal size. Adding that flag reduced the size of my binaries by ~35% with the (pretty big) drawback of increasing the compilation time by ~300%. In any case, if you're starting with yew + trunk you're lucky, since there is a fresh release that should guarantee that this line:

<link data-trunk rel="rust" href="Cargo.toml" data-bin="yew_lillaost" data-wasm-opt="z" />

will make wasm-opt-imizer work out of the box. In previous versions a separate installation of the toolchain was needed to make the optimizer work. An annoying little detail that took me a morning to detect.

Just remember to disable the call to the optimizer while in dev or you'll make trunk serve practically useless.

Shared State

The couple of times I used React I used redux. Nothing to think about, it was what every other developer in the team was using and it just made sense.

But the start of this project included too many moving parts to look for a redux-like library for yew. In case you're curious, I was interested in testing yewdux. For this first implementation of LO over Yew I decided to go as bare-bones as possible. I found a possible implementation that meets my needs based on 2 pieces:

  1. ContextProvider
  2. An event bus implemented with an Agent as can be seen in the pub - sub example.

ContextProvider

While reading the documentation, I found a brief mention to Yew Contexts. In the docs, the example is innocent enough, a simple read-only piece of data shared downstream.

But why not do something like sharing a Rc::RefCell:: "State"? As an example, this is the view code of my main component. This component mounts:

  1. A general ContextProvider.
  2. A responsive Navigation menu inside the header bar.
  3. A Router responsible to mount the content of the page. The content of the switch function can give you an idea of the internal structure.
  4. A footer with a couple of links.
fn switch(routes: &LillaOstRoutes) -> Html {
    match routes {
        LillaOstRoutes::Home => html! { <PageMain page=0 />},
        LillaOstRoutes::Page { no } => html! { <PageMain page=*no />},
        LillaOstRoutes::Settings => html! { <PageSettings /> },
        LillaOstRoutes::Summary => html! { <PageSummary />},
        LillaOstRoutes::Details { id } => html! { <PageDetails event_id=*id />},
        _ => html! { <PageNotFound /> },
    }
}

impl Component ... {
    fn view(&self) -> Html {
        html! {
            <div id="lilla-ost-root">
                <ContextProvider< Rc< RefCell< LillaOstState > > > context=self.state.clone()>
                    { self.view_nav() }
                    <div class="container">
                            <Router<LillaOstRoutes> render=Router::render(switch) />
                    </div>
                    <footer class="footer">
                        <div class="content has-text-centered">
                            { "Powered by " }
                            <a href="https://yew.rs">{ "Yew" }</a>
                            { " using " }
                            <a href="https://bulma.io">{ "Bulma" }</a>
                            { " by "}
                            <a href="https://twitter.com/jc_bellido">{ "jcbellido" }</a>
                        </div>
                    </footer>
                </ContextProvider<Rc< RefCell< LillaOstState > > > >
            </div>
        }
    }
}

My goal there was to express the idea that: anything hanging from here has the option to access the state. From that point I can access the state by using the link.context as can be seen here in the QuickInsert component (check the image at the top of this article)

impl Component for QuickInsert {
    type Message = MsgQuickInsert;
    type Properties = ();

    fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
        // Grab state here
        let (sto, _cal) = link
            .context::<Rc<RefCell<LillaOstState>>>(callback::Callback::noop())
            .unwrap();

        // Use it right here
        let first_active: Option<Person> =
            sto.borrow().individuals().into_iter().find(|p| p.is_active);
        [...]

This setup reduced the scaffolding I needed to write and the state is there when I need it.

Event Bus

The introduction of the ContextProvider solved the availability of the model to the components. Data was reachable and persisted properly. Everything was fine until the introduction of the Quick Insert control. It's simpler to understand with an image:

Quick Insert

In the original LillaOst that Add Feed button was actually navigating to the main page after clicking. Blunt perhaps, but rock solid. I didn't want to do the same in this version. The issue, as you can guess, was that the update in the model wasn't being properly communicated to any component observing it. In other words, to see the latest entry you were forced to do a full page reload.

Again, taking a look into Yew's examples we can find Pub Sub. The idea is to use an agent to forward messages to observers of changes in the model.

As an example, the DailySummary Component, as can be seen at the center of the image below, hooks itself to the EventBus like so:

impl Component for DailySummary {
    type Message = MsgDailySummary;
    type Properties = ();

    fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
        let producer = EventBus::bridge(link.callback(MsgDailySummary::StorageChanged));

        Self {
            link,
            node_ref: NodeRef::default(),
            producer,
        }
    }
    (...)
}

After the introduction of the Event Bus, pushing that Add Feed button resulted in the expected update in the same page without a navigation.

Quick Insert Result

To be continued

This is the first half of the article about LillaOst adoption of yew. In the next part I'll cover: