November 1, 2024

Announcing Sycamore v0.9.0

I’m excited to announce the release of Sycamore v0.9.0! This is once again by far the biggest update we’ve had.

Sycamore is a reactive Rust UI framework for building web apps using WebAssembly. This release is by far the biggest release we’ve ever had, with tons of new features and improvements. If you have not used Sycamore before, here’s a quick sample:

#[component]
fn Counter(initial: i32) -> View {
    let mut value = create_signal(initial);

    view! {
        button(on:click=move |_| value += 1) {
            "Count: " (value)
        }
    }

}

Sycamore’s community has also grown a lot since the v0.8.0 release. We’ve gone from just over 1.0k stars to 2.8k stars on GitHub. What used to be just over 350 discord members has now grown to 626! We’ve also reached 151k downloads on crates.io.

For migrating over from v0.8, check out the migration guide

A shiny new website

We now have a shiny new website along with a shiny new domain: sycamore.dev! This was long overdue. We were previously using a Netlify subdomain so this change hopefully makes Sycamore look more legitimate. The old website had a bunch of issues such as buggy navigation, no server side rendering support, and an awkward layout. This new website redesign fixes many of those things. The old docs are still available at the old website but the index page will now automatically redirect to the new website.

A lot of the content has also been rewritten and updated for this new version of Sycamore. This includes a brand new “Introduction” section which helps guide you through creating your first Sycamore app, a simple Todo manager. This introduces various topics such as the view macro, the basics of reactivity, and how rendering lists work. This will hopefully help new users interested in Sycamore to get started with the main concepts.

Here are a few comparisons between the old and new website.

old homepage
The old homepage
new homepage
The new homepage
old docs
The old docs
new docs
The new docs

There are still currently a few sections of the docs that needs writing or simply needs a few more details. You can help us out by contributing to the docs! Simply go to the relevant page and click on “Edit this page on GitHub” at the bottom and send us a Pull Request.

Reactivity v3

What is probably the biggest new feature of this release is our new reactivity system, dubbed Reactivity v3! In Reactivity v2 (introduced in the v0.8 release), we eliminated the need for cloning signals and other reactive primitives into closures. This, however, came at the expense of introducing lifetimes for tracking whether a signal was alive and could be accessed.

Lifetimes are well known to add complexity to a Rust codebase. So although we no longer needed to deal with cloning, we now needed to deal with lifetimes. Reactivity v3 fixes all this. We made all signals and other reactive data-types both 'static and Copy-able. This way, you get both the benefit of passing signals wherever you want without littering your codebase with .clone() everywhere, all without having to worry about lifetimes. Along the way, we also eliminated the need for the cx parameter as well!

Whereas previously, you might have written:

let signal = create_signal(cx, 123);
create_effect_scoped(cx, |cx| {
    let nested = create_signal(cx, 456);
    println!("{signal}, {nested}");
});

Now, you can simply write:

let signal = create_signal(123);
create_effect(move || {
    let nested = create_signal(456);
    println!("{signal}, {nested}");
});

Although a very contrived example, hopefully this demonstrates that the new reactivity system is much more simple and intuitive. We no longer need to thread the cx parameter everywhere, we no longer have to worry about scoped versus non-scoped effects, and we can pass signals wherever we want without infecting everything with lifetimes.

Under the hood, this involved a huge rewrite of essentially the entire sycamore-reactive crate from scratch. The new implementation uses a singleton Root datatype for managing the reactive graph instead of a bunch of smart pointers everywhere in a tangled mess. This should hopefully make the implementation more robust and reliable.

View v2

Another major change coming to Sycamore v0.9 is View v2. Reactivity v3 removed a lot of friction and boilerplate when interacting with reactive state. View v2 continues this theme and removes a bunch of boilerplate from components and views.

The biggest change is the complete removal of the GenericNode and Html traits which have been infesting Sycamore codebases ever since we introduced SSR (server side rendering) support all the way back in v0.5.

Witness the difference yourself. Here is Sycamore v0.8 code:

#[component(inline_props)]
fn Component<'a, G: Html>(cx: Scope<'a>, value: &'a ReadSignal<i32>) -> View<G> {
    ...
}

There is a bunch of noise here that is distracting from what this component does, such as the 'a lifetime and the G: Html generic parameter. Reactivity v3 and View v2 together turns this into:

#[component(inline_props)]
fn Component(value: ReadSignal<i32>) -> View {
    ...
}

Doesn’t this just look so much better?

New builder API

This refactor also introduces a new builder API. Apologies to all the churn the builder API has received over the past few releases, but I really think this new API is much better than before. For a long time, the builder API was always a second-class citizen compared to the macro. This is no more. In fact, the view! macro has been refactored to simply codegen the builder API behind the hood, making the builder API a true first-class citizen in Sycamore. Here is what it looks like:

div().class("hello-world").children((
    span().style("color: red").children("Hello "),
    em().children("World!"),
))

For more information, check out the builder API docs in the book.

Type-checked HTML attributes

Since we are now using the builder API as the codegen target for the view macro, we also get type-checked and auto-completed HTML attributes!

lsp hover for attributes
Documentation for attributes, provided by Rust-Analyzer in VSCode

This also means no more silly typos causing hard to spot bugs, and finally, proper support for boolean and optional attributes.

Event handlers are also fully type-checked now so that you can use the specific event type such as KeyboardEvent instead of the base Event type.

Attribute passthrough

Suppose you’re writing a component library and are creating a Button component. Which props should you component accept? Ideally, you want your component to be as flexible as possible so you should try to provide as many HTML attributes as you can. This quickly becomes tedious: you’ll need to provide class, id, disabled, r#type, value, etc. Furthermore, HTML allows arbitrary custom attributes of the form data-* as well as a bunch of accessibility attributes like aria-*, making this task essentially impossible.

Enter attribute passthrough. This allows your component to behave as if it were an HTML element, accepting HTML attributes, and letting you forward all of these attributes onto the element itself. Here’s an example:

#[component(inline_props)]
fn Button(
    #[prop(attributes(html, button))]
    attributes: Attributes,
    children: Children,
    accent: StringAttribute,
) -> View {
    view! {
        // Spread the attributes onto the wrapped element.
        button(..attributes) {
            (children)
        }
    }
}

// Now use your component just as if it were a normal HTML element.
view! {
    Button(
        class="btn btn-red",
        id="login-button",
        on:click=move |_| login(),
        // `accent` is passed as a prop, not as an attribute.
        accent="primary",
    ) {
        "Login"
    }
}

To learn more, read the section on Attribute passthrough in the book.

Resources

Sycamore v0.9 introduces the Resources API. Resources let you load asynchronous data into your app, in a way that is tightly coupled with the reactivity system and suspense.

Resources are essentially asynchronous nodes in the reactive graph. This means that resources can depend on reactive values. For instance, this will refetch the resource whenever the id signal is updated.

let id = create_signal(...);
let resource = create_resource(on(id, move || async move {
    fetch_user(id.get()).await
}));

You can then use the resource value like so:

view! {
    Suspense(fallback=move || view! { "Loading..." }) {
        (if let Some(data) = resource.get_clone() {
            view! {
                ...
            }
        } else {
            // This will never get shown.
            view! {}
        })
    }
}

Accessing the value will automatically trigger suspense, letting you easily define loading screens etc. To learn more, read the section on Resources in the book.

SSR streaming

We’ve had support for server side rendering (SSR) for quite a while now. This release, however, introduces SSR streaming. What is SSR streaming?

Let’s first look at how normal server side rendering works. If we don’t fetch any asynchronous data, everything is simple: just render the app in one shot on the server and send it over to the client. If, however, we do have asynchronous data, we have a few choices. We might choose not to do any data-fetching on the server and instead just send the loading fallback. We can then do all the data-fetching client side. This approach has a major disadvantage. When we make the request to the server, we already, in principle, know all the asynchronous data that needs to be fetched. The client, however, can not know this until the WASM binary has been sent over, loaded, and the app hydrated. So we are wasting a lot of time where we could have been fetching these asynchronous resources in parallel.

Another approach would be to load all the data on the server-side and wait for all loading to complete before sending the HTMl over to the client. Such an approach, however, causes an annoying delay on the client where nothing is displayed while the data is loading.

SSR streaming strikes a balance between these two approaches. First, an initial HTML shell is sent over to the client displaying the fallback view, such as loading text or spinners. Then as the data is fetched on the server, the new view is rendered and subsequently streamed over to the client over the same HTTP request. This new view is then dynamically inserted into the right position in the DOM.

SSR Streaming Demo

SSR streaming offers the best of both worlds. The client displays something right away, and data is fetched as soon as possible on the server and the result streamed over to the client.

This feature is seamlessly integrated with Suspense. The natural streaming boundaries are the suspense boundaries, so that the suspense fallback is sent first, and then when the suspense resolves, the suspense content is streamed over.

Learn more by reading the SSR Streaming section of the book.

Other changes

There have been so many other smaller changes since v0.8. Many were done so long ago that it’s hard to think they were done during the v0.9 development cycle. One of them is adopting a logo in #551!

Some of these changes, in fact, are already obsolete as they got replaced with new features and refactors later on. You can see the full changelog here for yourself.

The future of Sycamore

This release was long overdue. In fact, the last stable update (v0.8.2) was over 2 years ago! Such a long time between releases definitely is not healthy for an Open-Source project, and this is mainly my fault, partly due to a lack of time for working on Sycamore.

After this release, we’ll hopefully settle into a more regular release cycle of a couple months rather than years. This will also mean that subsequent updates will likely be smaller and contain less breaking changes. This is a good thing. It means that we’re getting to a point where we’re pretty happy with the look and feel of the current API and we can really start building things on top rather than constantly changing the foundations.

On a related note, Sycamore is looking for more contributors! If you’re interested in contributing code, check out some good first issues on our issue tracker. Contributing doesn’t just mean writing code. It can also be contributing to the docs, writing blog posts, or building community libraries for Sycamore.

Thanks!

A big thanks to everyone who contributed to this release! Listed here in no particular order: