Web — Rust and Leptos
These are the steps to set up and run a simple Rust Web app that calls into a shared core.
This walk-through assumes you have already added the shared
and shared_types
libraries to your repo, as described in Shared core and types.
There are many frameworks available for writing Web applications in Rust. Here we're choosing Leptos for this walk-through as a way to demonstrate how Crux can work with web frameworks that use fine-grained reactivity rather than the conceptual full re-rendering of React. However, a similar setup would work for other frameworks that compile to WebAssembly.
Create a Leptos App
Our Leptos app is just a new Rust project, which we can create with Cargo. For
this example we'll call it web-leptos
.
cargo new web-leptos
We'll also want to add this new project to our Cargo workspace, by editing the
root Cargo.toml
file.
[workspace]
members = ["shared", "web-leptos"]
Now we can cd
into the web-leptos
directory and start fleshing out our
project. Let's add some dependencies to shared/Cargo.toml
.
[package]
name = "web-leptos"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { version = "0.6.15", features = ["csr"] }
shared = { path = "../shared" }
[lints.clippy]
empty_docs = "allow"
If using nightly Rust, you can enable the "nightly" feature for Leptos. When you do this, the signals become functions that can be called directly.
However in our examples we are using the stable channel and so have to use
the get()
and update()
functions explicitly.
We'll also need a file called index.html
, to serve our app.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Leptos Counter</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>
</head>
<body></body>
</html>
Create some UI
There is slightly more advanced example of a Leptos app in the Crux repository.
However, we will use the
simple counter example,
which has shared
and shared_types
libraries that will work with the
following example code.
Simple counter example
A simple app that increments, decrements and resets a counter.
Wrap the core to support capabilities
First, let's add some boilerplate code to wrap our core and handle the
capabilities that we are using. For this example, we only need to support the
Render
capability, which triggers a render of the UI.
This code that wraps the core only needs to be written once — it only grows when we need to support additional capabilities.
Edit src/core.rs
to look like the following. This code sends our
(UI-generated) events to the core, and handles any effects that the core asks
for. In this simple example, we aren't calling any HTTP APIs or handling any
side effects other than rendering the UI, so we just handle this render effect
by sending the new ViewModel to the relevant Leptos signal.
Also note that because both our core and our shell are written in Rust (and run in the same memory space), we do not need to serialize and deserialize the data that we pass between them. We can just pass the data directly.
use std::rc::Rc;
use leptos::{SignalUpdate, WriteSignal};
use shared::{Counter, Effect, Event, ViewModel};
pub type Core = Rc<shared::Core<Effect, Counter>>;
pub fn new() -> Core {
Rc::new(shared::Core::new())
}
pub fn update(core: &Core, event: Event, render: WriteSignal<ViewModel>) {
for effect in core.process_event(event) {
process_effect(core, effect, render);
}
}
pub fn process_effect(core: &Core, effect: Effect, render: WriteSignal<ViewModel>) {
match effect {
Effect::Render(_) => {
render.update(|view| *view = core.view());
}
};
}
That match
statement, above, is where you would handle any other effects that
your core might ask for. For example, if your core needs to make an HTTP
request, you would handle that here. To see an example of this, take a look at
the
counter example
in the Crux repository.
Edit src/main.rs
to look like the following. This code creates two signals
— one to update the view (which starts off with the core's current view), and
the other to capture events from the UI (which starts of by sending the reset
event). We also create an effect that sends these events into the core whenever
they are raised.
mod core;
use leptos::{component, create_effect, create_signal, view, IntoView, SignalGet, SignalUpdate};
use shared::Event;
#[component]
fn RootComponent() -> impl IntoView {
let core = core::new();
let (view, render) = create_signal(core.view());
let (event, set_event) = create_signal(Event::Reset);
create_effect(move |_| {
core::update(&core, event.get(), render);
});
view! {
<section class="box container has-text-centered m-5">
<p class="is-size-5">{move || view.get().count}</p>
<div class="buttons section is-centered">
<button class="button is-primary is-danger"
on:click=move |_| set_event.update(|value| *value = Event::Reset)
>
{"Reset"}
</button>
<button class="button is-primary is-success"
on:click=move |_| set_event.update(|value| *value = Event::Increment)
>
{"Increment"}
</button>
<button class="button is-primary is-warning"
on:click=move |_| set_event.update(|value| *value = Event::Decrement)
>
{"Decrement"}
</button>
</div>
</section>
}
}
fn main() {
leptos::mount_to_body(|| {
view! { <RootComponent /> }
});
}
Build and serve our app
The easiest way to compile the app to WebAssembly and serve it in our web page
is to use trunk
, which we can install with
Homebrew (brew install trunk
) or Cargo
(cargo install trunk
).
We can build our app, serve it and open it in our browser, in one simple step.
trunk serve --open