Valerie: Rethinking Web Apps in Rust

Emmanuel Antony - Jul 10 '20 - - Dev Community

I have personally tried and seen various front-end frameworks for Web Apps in Rust. To be honest, they are pretty good and do their job well. But they do have some drawbacks, like a steep curve for understanding the API, and your code getting more complex as your app grows in size.

Now when I see the JS side of things, I see easy adoption and usage. But nah, I'm a Rust fan.

How to make things better? Find flaws and fix them. 😁

Flaws

A natural way of writing UI 🤔

I felt the lack of a natural way of writing UI. Writing UI should not be hindered by other factors like the language features itself. Like implementing a Trait and all functions along with it to display something simple as a Hello, World!, felt a bit cumbersome.

Message passing is the way to go but not matching them 🥺

Matching messages to functionality, using a match, might be an extra boilerplate that we can aim to reduce.

Virtual DOM 🤨

Current computers are fast enough, that differences between using a Virtual DOM and not using it, are too far less to notice. But yes, if it reduces performance, we can try to get rid of it.

Data centric and composable 😮

It is a lot easier to build composable UI. Why should the feature not be there?

So, what's next?

I looked at all this and thought, let's write a hypothetical UI first which might use a hypothetical library and then try implementing the library in Rust.

fn ui() -> Something {
    div("Hello, World!")
}

fn run() {
    App::render(ui)
}
Enter fullscreen mode Exit fullscreen mode

How should we manage variables and states? It should be something like this and it should somehow magically update.

fn ui() -> Something {
    let x = 0;

    div(
        x,
        button("Add 1").on_click(|| { x += 1; }),
    )
}
Enter fullscreen mode Exit fullscreen mode

And how would we bind data to inputs?

fn ui() -> Something {
    let text = String::new();

    div(
        text,
        input("text").bind(text),
    )
}
Enter fullscreen mode Exit fullscreen mode

Looking at these snippets, pretty much basic functionality is thought of. Our plan is to implement State variables, update them when changed and bind them to inputs. Now let's come back to implementing the library.

Rethinking States

  • State update should be asynchronous. Period.
  • We are aiming for a message passing system, which passes messages to the receiver when the value is updated, and the receiver will be attached to elements and update their value as and when necessary.
  • This means we don't need diffing or any Virtual DOM.
  • It should also be bindable to elements and update when an input changes (and also be double way bindable).
  • It should also be derivable, i.e. a state can derive values from other states, i.e. if State A is deriving its value from State B, when State B updates its value, State A should also do the same.

The solution I am trying to build: Valerie

Enter Valerie. The idea behind Valerie was to enable people to build clean web UI with Rust.

Let's start with "Hello, World!".

use valerie::prelude::components::*;
use valerie::prelude::*;

fn ui() -> Node {
    h1!("Hello, World!").into()
}

#[valerie(start)]
pub fn run() {
    App::render_single(ui());
}
Enter fullscreen mode Exit fullscreen mode

The above code when compiled, is a .wasm file of 14.8KB (6.8KB gzip).

Let's have a look at states.

fn ui() -> Node {
    let string = StateMutex::new(String::new());
    let length = StateAtomic::from(&string, |x| x.len());

    div!(
        h3!(string.clone()),
        h3!(length),
        input!("text").bind(string)
    )
    .into()
}
Enter fullscreen mode Exit fullscreen mode

This will show an input which binds to a String, and the length variable derives its value from the string State. And when you type something in the input text field, the data is updated magically.

The above code when compiled, is a .wasm file of 44.7KB (15.1KB gzip).

One thing to notice here is StateAtomic and StateMutex, they are for types implementing Copy and Clone respectively. Internally StateAtomic uses atomics for achieving concurrency and StateMutex uses a mutex.

What if you want to click a button and increment some counter variable?

fn ui() -> Node {
    let count = StateAtomic::new(0);

    div!(
        h3!(count.clone()),

        button!("Click Me!")
        .on_event("click", count, |x, _| {
            *x += 1;
        })
    )
    .into()
}
Enter fullscreen mode Exit fullscreen mode

The above code when compiled, is a .wasm file of 33.1KB (13.5KB gzip).

These are some of the many features of Valerie that I have showed here. The library is still in a very early phase.
Valerie is also a no-std library.

Every piece of UI like a String or div implements the Component trait. You can even extract a part of UI into a different function which returns an impl Component and then reuse it when needed.

Rust follows the principle of Zero-cost abstractions, i.e. you don't have to pay the price of performance for the library features you don't use and whatever you do use you couldn't hand code any better. This means if you don't use a feature provided in Valerie, then it won't end up in the compiled .wasm file.

And comparing WASM with JS, after being received by the browser, your browser has to convert JS into an AST, compile the necessary functions and run it. Now, that's a lot of work. But in the case of WASM, your browser can directly run it using a JIT compiler, as your .wasm file is being received. Because a .wasm file is an optimised portable binary, the browser doesn't have to waste resources optimising it.

Thank you for reading through the article and do take a look at Valerie.

GitHub logo emmanuelantony2000 / valerie

Rust front-end framework for building web apps

Valerie

CI License Cargo Documentation Discord

Rust front-end framework for building web apps.

Valerie is still in a very early phase A lot of features are not available at the moment A lot of work is left and you are welcome to try it out.

  • No Virtual DOM.
  • UI can be made in a simple manner by following an MVVM architecture rather an MVC architecture.
  • Use state variables to update the UI where required.
  • Written without any unsafe code.

Architecture

  • Every UI element has to implement the Component trait.
  • A page is a function which returns a Node.
  • Two type of State variables
    • StateAtomic for types implementing Copy.
    • StateMutex for types implementing Clone.

Setting up

  • Run cargo new --lib some_name
  • Add valerie to the dependencies
  • Create a static directory and create an index.html inside it
<!doctype html>
<html lang="en">
    <head>
        <meta charset="
Enter fullscreen mode Exit fullscreen mode
.
Terabox Video Player