WASM with Rust (and Leptos)

I’m going to be going through some of the steps from Leptos Getting Started. Hopefully we’ll be able to add something here.

Prerequisites

We’re going to use Trunk to run our application, so first off we need to make sure we’ve installed it

cargo install trunk

Trunk allows us to build our code, run a server up and run our WASM application, moreover it’s watching for changes and so will rebuild and redeploy things as you go. Web style development with a compiled language. We’ll cover more on trunk later.

Creating our project

Create yourself a folder for your application and run the terminal in that folder.

We need to create our project – although I use RustRover from JetBrains, let’s go “old school” as use cargo to create our project etc.

Run the following (obviously change wasm_app to something more meaningful for your app name

cargo new wasm_app --bin

cd into the application (as above, mine’s named wasm_app).

cargo add leptos --features=csr

We’ll need to add the WASN target

rustup target add wasm32-unknown-unknown

in the root folder (where your Cargo.toml file is) create an index.html with the following

<!DOCTYPE html>
<html>
  <head></head>
  <body></body>
</html>

Next create a cd into the src folder and edit the main.rs – it should look like the following

use leptos::prelude::*;

fn main() {
    leptos::mount::mount_to_body(|| view! { <p>"Hello, world!"</p> })
}

The mount_to_body, as the name suggests, essentially injects your WASM code into the <body></body> element.

Now, from the root folder (i.e. where index.html is) run

trunk serve --open

If the default port (8080) is already in use we can specify the port using

trunk serve --open --port 8081

If all went well then you see your default browser showing the web page, if not then open a browser window and navigate to http://localhost:8081/

To save having to set the port via the CLI, you can also create a trunk.toml file in the root folder with something like this in it

[serve]
address = "127.0.0.1"
port = 8081

Taking it a bit further

We’ve got ourselves a really simple WASM page.

Let’s move this a little further by creating a component for our application.

Create a file in src named app.rs and we’ll add the following

use leptos::prelude::*;

#[component]
pub fn App() -> impl IntoView {
    view! {
        <p>"Hello, world!"</p>
    }
}

and change the main.rs to this

mod app;

use leptos::mount::mount_to_body;
use crate::app::App;

fn main() {
    mount_to_body(App);
}

If you kept trunk running it will automatically rebuild the code and refresh the browser.

Components

The component (below) returns HTML via the view macro and as you can see, this returns a trait IntoView. As you can see we mark the function as a #[component]

#[component]
pub fn App() -> impl IntoView {
    view! {
        <p>"Hello, world!"</p>
    }
}

Note: you might want to change your text to prove to yourself that the did indeed get updated in the web page when you saved it – assuming trunk was running.

This is a pretty simple starting point, so let’s add some more bits to this…

Let’s change the App function to this

use leptos::prelude::*;
 
#[component]
pub fn App() -> impl IntoView {
    let (count, set_count) = signal(0);

    view! {

        <button on:click=move |_| set_count.set(count.get() + 1)>Up</button>
        <div>{count}</div>
        <button on:click=move |_| set_count.set(count.get() - 1)>Down</button>
    }
}

The signal (which is a reactive variable) may remind you of something like useState in React, we deconstruct the signal (which has the default of 0) into a count (getter) and a set_count (setter). To be honest the set and get functions seem odd if you’re using the properties such as C#, but that’s the way it is is Rust.

The count value is of type ReadSignal and set_count is of type WriteSignal.

We can also set the value of count using the following within the closure. Ultimately this should be a more performant way of doing things. The example above might be preferred for readability (although that’s debatable) – it does however look more inline with the way we get values etc. I’ll leave others to debate the pros and cons, for me the line below is effecient.

*set_count.write() += 1

Routing

Let’s rename the app.rs file to counter.rs (also rename the function to Counter) and create a new app.rs file which will acts as the router to our components. We’ll need to add this to the Cargo.toml dependencies

leptos_router = "0.8.5"

and in the app.rs paste the following code

use leptos::prelude::*;
use leptos_router::{
    components::{Route, Router, Routes},
    StaticSegment,
};
use crate::counter::Counter;
use crate::home::Home;

#[component]
pub fn App() -> impl IntoView {
    view! {
        <Router>
            <Routes fallback=|| "Page not found.">
                <Route path=StaticSegment("") view=Home />
                <Route path=StaticSegment("counter") view=Counter />
            </Routes>
        </Router>
    }
}

You’ll need to add the mod to the main.rs file to include the home reference.

I’ve also added a home.rs file with the following

use leptos::prelude::*;

#[component]
pub fn Home() -> impl IntoView {
    view! {
        <p>Welcome to your new app!</p>
    }
}

As you can see, the router routes / to our Home component and the /counter to our Counter component.

Meta data from code

Whilst we have an index.html which you can edit, we might want to supply some of the meta data etc. via the app’s code.

Add the following to Cargo.toml dependencies

leptos_meta = "0.8.5"

Now in main.rs add

use leptos::view;
use leptos_meta::*;

and now change the code to

fn main() {
    mount_to_body(|| {
        provide_meta_context();
        view! { 
            <Title text="Welcome to My App" />
            <Meta name="description" content="This is my app." />
            <App />
        }
    });
}

The provide_meta_context() function allows us to inject metadata such as <title>, <meta> and <script>

Code

Code for this post is available on GitHub.