June 12, 2023

Elegant Web UIs with Dioxus in Spin

Ivan Towlson Ivan Towlson

spin rust

Elegant Web UIs with Dioxus in Spin

Dioxus is a user interface library for Rust, inspired by React, and able to target Web applications, cross-platform desktop applications, and even the terminal. It describes itself as “focused on developer experience,” which piqued my interest. (It also promises “beautiful” applications, which is a dangerous promise when I’m writing the code.) Could you use it to write Spin applications, though? Well, at the risk of spoilers, this post will show you how.

Hello, Dioxus

Dioxus has numerous ways to render user interfaces: to WebAssembly (Wasm) based single-page Web applications (SPAs), as desktop or mobile applications, as terminal applications, or as good old fashioned HTML. I’ll concentrate on that last one.

If you’re interested in building and deploying Dioxus single-page applications, check out this example of Dioxus client-side rendering in Spin

Let’s dive in, starting with a fresh Spin http-rust application:

$ spin new http-rust dioxus-demo --accept-defaults
$ cd dioxus-demo

To use Dioxus to generate HTML in Spin, we need to add not just the core dioxus crate, but also the dioxus-ssr crate, which provides server-side rendering (SSR):

$ cargo add dioxus
    Updating crates.io index
      Adding dioxus v0.3.2 to dependencies.
$ cargo add dioxus-ssr
    Updating crates.io index
      Adding dioxus-ssr v0.3.0 to dependencies.

Dioxus is not yet stable. This post was written using Dioxus 0.3. The authors don’t expect the server-side rendering API to change drastically, but you might need to adapt things if you’re on a later version.

Now we can dive into the code! Here’s the skeleton of the generated Spin component, once we’ve cleaned out the sample code:

use anyhow::Result;
use spin_sdk::{
    http::{Request, Response},
    http_component,
};

#[http_component]
fn handle_dioxus_demo(_req: Request) -> Result<Response> {
    todo!()
}

Putting that on hold for a moment, the next step is to write a function that generates a simple HTML body. The Dioxus SSR documentation is a great starting point. Dioxus provides an rsx! macro, inside which you can describe HTML elements using a more Rusty syntax; if you’re familiar with React, it’s similar to embedding JSX in JavaScript. Again, Dioxus has several options for rendering rsx! elements, but for Spin, the simplest solution is to render it directly, as in the first example in the SSR docs:

use dioxus::prelude::*;

fn hello() -> String {
    dioxus_ssr::render_lazy(rsx! {
        div { "Hello from Dioxus running on Spin" }
    })
}

As you can see, render_lazy turns an rsx! expression into a HTML string. That makes it super easy to write the Spin HTTP component:

#[http_component]
fn handle_dioxus_demo(_req: Request) -> Result<Response> {
    let body = hello();
    Ok(http::Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body(Some(body.into()))?)

}

Run spin build --up and try it out in a Web browser:

Hello

Yes, well. I guess you can’t take the engineer out of the user interface.

Components Inside Components Inside Components

Dioxus, like React, builds up user interfaces by composing them out of components, which can of course themselves be composed out of components. Dioxus components are not Spin components! (Thinking of names other than “component” is apparently one of the three hard problems of software engineering.) To avoid confusion, I’ll qualify “component” when I use it.

A Dioxus component is a function from dioxus::prelude::Scope to dioxus::prelude::Element. Note that the hello function above is not a component - it doesn’t accept a scope, and it produces a raw HTML string, rather than an abstract node. Here’s a super simple component:

fn UserList(cx: Scope) -> Element {
    cx.render(rsx! {
        div { "No users defined!" }
    })
}

Rust function names are usually snake_case, but Dioxus expects component names to be PascalCase. If you snake-case your Dioxus components, you’ll need to qualify references to them, e.g. self::not_found. If you PascalCase your Dioxus components, you’ll want to set #![allow(non_snake_case)] so that the Rust compiler doesn’t shout at you.

Dioxus components can and usually do have properties. You can read about those in the Dioxus documentation. All I wanted to do here was introduce the idea of components, so as to move on to…

Routing with Dioxus

Dioxus includes a router for dispatching requests to Dioxus components based on the URL path. For many cases, you may prefer to use the router provided by the Spin SDK, which allows dispatching on the HTTP method as well as the path. But the Dioxus router has its own nice features, such as composition with HTML elements, link generation, and nested routes. So I’ll quickly share how to get it set up, then you can figure out which best suits your needs.

First add the dioxus_router crate to the Spin application:

cargo add dioxus-router

The Dioxus router needs to know the full request URL. Spin passes the full request URL in the spin-full-url header, and the Router receives it via an initial_url attribute. So the body of the Spin component now looks like this:

#[http_component]
fn handle_dioxus_demo(req: Request) -> Result<Response> {
    let url = req.headers().get("spin-full-url").unwrap().to_str()?;

    let router = rsx! {
        Router {
            initial_url: url.to_owned()
            Route { to: "/home", Home {} }
            Route { to: "/users", UserList {} }
            Route { to: "", NotFound {} }
        }
    };

    let body = dioxus_ssr::render_lazy(router);
    Ok(http::Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body(Some(body.into()))?)
}

The Rust req.uri() method returns only the path. Dioxus needs the full URL, so remember to use the header!

Here, Home, UserList and NotFound are Dioxus components, as discussed above.

To learn about other Dioxus router features such as composition and links, see the documentation.

Spin, Dioxus and Application Building

This has been a super brief introduction to Dioxus. I’ve not touched on features like interactivity, or on how to propagate state around a Dioxus application. If you’re interested, do read the docs. What I want to cover is what a Dioxus/Spin application might look like.

Dioxus is specifically a user interface library. It’s about building HTML pages. Spin is more general purpose. It’s about efficiently handling HTTP requests using Wasm. (And other events, but we’ll pass over that here.)

So if, for example, your application naturally falls into a front end and an API layer, you might find it congenial to break this into multiple Spin components according to responsibility:

  • For the front end HTML, a single Spin component, assigned to the /... route, that uses Dioxus components and the Dioxus router to compose its pages.
  • For the front end’s static assets, such as images and CSS, the off-the-shelf Spin static file server, assigned to a /static/... route or to distinct /images/... and /styles/... routes.
  • For the API layer, a hand-crafted Spin component assigned to /api/... (or multiple topic-specific Spin components assigned to routes like /api/account/... and /api/cart/...), containing the business logic, and handling storage and communication with third-party APIs.

You could, indeed, build your front end as a Dioxus Wasm SPA, and serve it using the static file server; or jump-start a mobile app using the Dioxus desktop/mobile and your existing Dioxus front end components, still talking to the same Spin API layer.

If you want to chat more about running Dioxus on Spin or on Fermyon Cloud, drop in to the Fermyon Discord server, or say hi on Twitter @fermyontech and @spinframework!

 

 

 


🔥 Recommended Posts


Quickstart Your Serveless Apps with Spin

Get Started