Introduction

What is avalanche?

avalanche is a platform-agnostic UI library aiming to bridge the strong typing, efficient runtime performance, and cross-platform prowess of Rust with the ergonomics of existing UI libraries like React and Svelte.

Like React and Vue, this library allows writing your UI code declaratively, with the ease-of-use of HTML but with dynamic updates. Unlike those libraries, however, avalanche analyzes your code at compile time to eliminate the need for most runtime comparisons and updates, meaning you get declarative ergonomics with imperative performance.

While we believe the foundations for these goals are strong, the library is still very much a work in progress, and the implementation may not always reach our standards yet. As such, while this library is useable for hobbyist projects, it is NOT production ready.

What is avalanche_web?

avalanche_web is the official Web platform interface for avalanche, allowing you to use avalanche in browsers and Electron apps by compiling your app to WebAssembly. It may be missing some listeners and other functionality as of now, but all HTML tags are available for use as components.

Using this tutorial

Before using this tutorial, you should first have a decent grasp of the Rust programming language. This tutorial also heavily uses avalanche_web, so you should be familiar with basic HTML.

This tutorial is new and still rough, so if you have any confusion or see a potential error, we'd be grateful if you created an issue.

Getting started

Prerequisites

Before you get started, ensure you have cargo and npm installed.

Creating an app

To create an avalanche_web app, first install trunk if you haven't already: cargo install trunk.

Next, create a binary application with cargo new <my-app-name>. Within the root of that directory, create an index.html file with these contents:

<html>
  <head>
  </head>
  <body>
  </body>
</html>

This serves as a template for trunk to generate an entrypoint index.html file for your application, along with providing us scaffolding to supply app metadata and the like.

With that basic setup out of the way, it's time to actually integrate avalanche-web. In the Cargo.toml file, add the following dependencies:

avalanche = "^0.1.0"
avalanche_web = "^0.1.0"

Next, add this code somewhere outside the main function:


#![allow(unused)]
fn main() {
use avalanche::{component, View};
use avalanche_web::components::{H1, Text};

#[component]
fn HelloWorld() -> View {
    H1(self, [
        Text(self, "Hello world!")
    ])
}
}

Finally, add this call in main:

use avalanche::{component, View};
use avalanche_web::components::{H1, Text};

#[component]
fn HelloWorld() -> View {
    H1(self, [
        Text(self, "Hello world!")
    ])
}
fn main_dont_run() {
avalanche_web::mount_to_body::<HelloWorld>();
}

Now, to see your app running in the browser, execute trunk serve --open within the my-app-name directory. If you haven't gotten any compiler errors, you should see your first avalanche and avalanche_web web app!

Explaining hello world

So what exactly is going on in the code above? Well, the most important aspect here is the #[component] attribute macro. #[component] accepts a function returning a View, which is the result of rendering a component. Within a function marked with #[component], functions whose names begin with a capital ASCII character and accept the component context self are interpreted as special component calls. Text is a component with a single parameter, so by specifying that, along with the context self, we get an instance of View with that text component. H1 can take an array of children, so this component here is equivalent to writing this raw HTML:

<h1>
    Hello world!
</h1>

Finally, we pass our new HelloWorld component as a type argument to avalanche_web::mount_to_body, which, as the name implies, renders our HelloWorld component within the <body></body> of our web app.

Basic components

Components are the building blocks of avalanche apps. They're represented as functions since they're fundamentally meant to be pure: they take in inputs and describe a UI as their output. They're so fundamental that this book is largely a component tutorial.

Creating components

Though components are written as functions, avalanche implements some quality-of-life and performance features not possible with functions. This requires the use of specialized syntax. If we call a component that takes no parameters called Comp, for example, we'll call it with Comp(self). These component calls only work within the bodies of other components, and they return the View type.

Passing parameters to components

Most components have parameters, and they're named. For example, in the components below, we pass href by name. However, the last parameter in a function definition can be supplied without a name as the last parameter in a call. For Text, that's the text parameter, which takes something convertible to a Cow<'_, str>, meaning you can pass it either a String or a &str. For every other avalanche_web component, the unnamed last parameter is children, which takes an Into<Vec<View>>. Below are two versions of a link component. One passes text and children explicitly, while the other does it implicitly, as is idiomatic.


#![allow(unused)]
fn main() {
use avalanche::{component, View};
use avalanche_web::components::{A, Text};

const FIRST_SITE: &str = "http://info.cern.ch/hypertext/WWW/TheProject.html";

#[component]
fn LinkExplicit() -> View {
    A(
        self,
        href = FIRST_SITE,
        children = [
            Text(self, text = "The first website")
        ]
    )
}

#[component]
fn LinkImplicit() -> View {
    A(
        self,
        href = FIRST_SITE,
        [
            Text(self, "The first website")
        ]
    )
}
}

For avalanche_web components, it's generally recommended to use implicit last parameters, so we'll be using them from now on in this tutorial. In third-party components, by convention, you should only use them if there is only one parameter, like in Text, or if the last parameter is a child or children of the component.

Receiving parameters and tracking

So far, we've dealt only with components with hard-coded data. However, within most practical components, we want to allow other components to pass them data. To enable that, you simply need to add parameters to your component function. Here, let's say we want a reusable Link with its own custom functionality, and want to allow a custom destination and text to be specified. We can add to and text parameters:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, View};
use avalanche_web::components::{A, Text};

#[component]
fn Link(to: &str, text: &str) -> View {
    A(
        self,
        href = tracked!(to),
        [
            Text(self, tracked!(text))
        ]
    )
}
}

Notice that for the href and implicit text parameters, we pass tracked! parameters instead of just passing them by value. That's because when we say a parameter is, for example, a &str, we actually receive a Tracked<&str>. A Tracked value wraps an inner value with data on whether it's been updated since last render. avalanche uses this to allow for efficient re-rendering of components. Calling tracked! on a Tracked value gives us its inner value. Since Text and A expect &str-like values, but the to and text parameters are Tracked<&str>, we use tracked!() to get &str values.

We can then construct Link inside of other components:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, View};
use avalanche_web::components::{Text, Div};
use avalanche_web::components::A;

#[component]
fn Link(to: &str, text: &str) -> View {
    A(
        self,
        href = tracked!(to),
        [
            Text(self, tracked!(text))
        ]
    )
}

#[component]
fn Example() -> View {
    Div(self, [
        Link(
            self,
            to = "https://example.com",
            text = "example.com"
        ),
        Text(self, " is a domain reserved for use in demos.")
    ])
}
}

When rendered, the above is equivalent to the HTML below.

<div>
    <a href="https://example.com">
        example.com
    </a>
    &nbsp;is a domain reserved for use in demos.
</div>

Within component calls, parameter order does not matter (except for what the implict last parameter is), so we could've also called Link with to and text swapped:

Link(
    self,
    text = "example.com".into(),
    to = "https://example.com".into()
)

Parameter type restrictions

All parameters must implement Clone. Note that all non-mut references implement Clone, making this restriction generally easy to deal with in practice.

Hooks, State, and Control Flow

Earlier, we described components as pure functions. Hooks allow us to introduce external data, state, and other impure data into our components.

Hooks

So far, all the data we've passed to parameters and used in general was static, specified at compilation time. But in real apps, we usually need to maintain state for things like network request statuses and inputs.

Let's say we're trying to write a counter. We'll start out with a stateless counter:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, View};
use avalanche_web::components::{Div, H2, Button, Text};

#[component]
fn Counter() -> View {
    Div(self, [
        H2(self, [
            Text(self, "Counter!")
        ]),
        Button(
            self,
            on_click = |_| todo!(),
            [
                Text(self, "+")
            ]
        ),
        Text(self, 0.to_string())
    ])
}
}

Since we don't keep track of the count yet, we've substituted it with the value 0. Text's text parameter accepts any value that implements ToString, so we can pass 0 instead of "0". We've also defined the on_click handler for Button, so the given closure's code will run on every click. Since right now we have no state to update yet, we call todo!(), so clicking the button would crash our app.

Now that we have our starting point, let's finally inject some state. The state hook allows us to inject state so we can keep track of the number of clicks. Since we have a count that monotonically increases, we can use an unsigned integer u64 with state<u64, _>.

Let's finally make our counter stateful with the state hook:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, state, View};
use avalanche_web::components::{Div, H2, Button, Text};

#[component]
fn Counter() -> View {
    let (count, set_count) = state::<u64>(self, || 0);

    Div(self, [
        H2(self, [
            Text(self, "Counter!")
        ]),
        Button(
            self,
            on_click = move |_| set_count.update(|count| *count += 1),
            [
                Text(self, "+")
            ]
        ),
        Text(self, tracked!(count).to_string())
    ])
}
}

Hooks are function calls that take self as their first parameter. This allows #[component] to know the function call is a hook, allowing it to pass in some internal state. Hooks have a call site identity, so the above state call will return a reference to the same state every render. A different state call elsewhere in the code, however, would reference different state.

The state hook takes the context self and a function providing the state's default value for u64 and returns a tuple with a reference to the current state (which we name count) and a setter, which we name set_count. The setter has an update method which accepts a closure that receives a &mut u64 and modifies it. Every time the user clicks on the button, on_click fires, calling set_counter.update(), which runs *count += 1, updating the state and causing the component to be rerendered.

Note that the reference returned is tracked, so here we get Tracked<&u64> instead of &u64. This is where the tracked system becomes useful. Constant values, when passed into components, are never considered updated. But if we called set_count.updated since the last time we called state, on the next call of state, count will be marked as updated. That lets avalanche know we should update the text of Text(self, tracked!(count).to_string()) in the UI.

The state returned by each call site of state is unique. If we call state in two different places, their values aren't linked.

If you've used React before, you might be familiar with the rules of hooks. Those don't apply here: feel free to call hooks within if, loops, and other complex control flow! You do still need to keep hook calls inside of components, but that's enforced at compile time.

With that, we have our first stateful component! If we instead wanted to simply set the state to a value like 0, we could write set_count.set(0) as a shorthand for set_count.update(|count| *count = 0).

Dynamic rendering

Oftentimes, we don't just want to update property values based on changes to state and props, but also what children we render. In some frameworks, that requires special template syntax, but we can take advantage of the fact that the Component!() syntax returns plain Rust values to use normal control flow instead.

Conditional rendering

Want to render something only if a condition is true? Use an if statement:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, View};
use avalanche_web::components::Text;

#[component]
fn Conditional(cond: bool) -> View {
    if tracked!(cond) {
        Text(self, "True!")
    } else {
        ().into()
    }
}
}

From this point, we'll be omitting previously used avalanche and avalanche_web imports for brevity.

The () type is a special Component that renders nothing. We call .into() on it to turn it into a View.

There is also a shorthand for this some component-or-nothing case:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, View};
use avalanche_web::components::Text;

#[component]
fn Conditional(cond: bool) -> View {
    tracked!(cond).then(|| Text(self, "True!")).into()
}
}

That's equivalent to the above example. This uses bool's then method. Option<View> is also a special component, rendering the View in its Some case and rendering nothing in its None case. Of course, if you want more complex conditionals, you can use other control flow like else if and match.

Tracking

The previous chapter showed how even with state, we can write our UI declaratively. But how, exactly, does this work? In theory, avalanche could rerender our entire app on every state change, but in practice, that's often too inefficient. Instead, avalanche uses the tracked system. Parameters and many hook return values are Tracked, and expressions with tracked!() calls also have a value of type Tracked. Then, avalanche only marks child parameters as updated if their input had a tracked!() value that was updated.

Handling impurity

The tracked system relies on purity, or the idea that a component's output should only be the result of its input parameters. Not doing that will lead to some issues. Take this first naive, impure attempt at introducing a random number to a component, using the rand crate's random function, which uses thread-local state in its implementation.

Here, we'll try to show a random number, and generate a new one every time we click a button.


#![allow(unused)]
fn main() {
use avalanche::{component, state, View};
use avalanche_web::components::{Text, Div, Button};
use rand::random;

#[component]
fn Random() -> View {
    let (_, trigger_update) = state(self, || ());

    Div(
        self,
        [
            Button(
                self,
                on_click = move |_| trigger_update.set(()),
                [
                    Text(self, "Generate new number")
                ],
            ),
            Div(self, [
                Text(self, random::<u16>().to_string())
            ]) 
        ]
    )
}
}

However, if you try out the example for yourself, you'll notice that the number doesn't actually update. A reasonable explanation for this would be we're not actually modifying trigger_update's state, or that its current value is equal to the previous one. However, calling a state setter always triggers a component rerender. Rather, the reason is that random::<u16>() is not Tracked, so it's considered a constant. This leads to an important rule: a component's parameter will only update if it has a tracked!() call with an updated Tracked value. Text(self, random::<u16>().to_string()) doesn't have a tracked!() call, so it will never update.

The issue here is that random is an impure function, but avalanche doesn't know that. The solution is to use hooks to contain impurity, like we did with the counter example.


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, state, View};
use avalanche_web::components::{Text, Div, Button};
use rand::random;

#[component]
fn Random() -> View {
    let (value, set_value) = state(self, || random::<u16>());

    Div(
        self,
        [
            Button(
                self,
                on_click = move |_| set_value.set(random()),
                [
                    Text(self, "Generate new number")
                ]
            ),
            Div(self, [
                Text(self, tracked!(value).to_string())
            ]) 
        ]
    )
}
}

Now, text clearly depends on the value state, so when we call set_value, avalanche will update that text value on screen. Using hooks introduces impurity in a manner avalanche can understand.

Rules of tracked

This example leads us to the three main tracked rules:

Avoid mutable variables in components

Mutable variables make the tracked system leaky. Consider this snippet of code:

let (value, _) = state(self, || 5);
let mut mutable = 2;
mutable += *tracked!(value);

Since mutable isn't tracked, but its value depends on a Tracked value, using it as a component parameter will cause unexpected failures to update UI appearance. Instead, prefer creating new variables.

Wrap interior mutability and hidden side effects

For parameter values, directly using values like rand::random() is a problem because they depend on some external state that avalanche is not aware of, leading to stale values displayed on rerender. This also applies to values like Rc<RefCell<T>> if they are updated outside of hook functionality. When using interior mutability, then, make sure to wrap it in state, and perform updates within the set or update methods.

Avoid third-party macros

Macros can lead to convenient code; for example, Text(self, format!("Hello, {} {}!", tracked!(title), tracked!(name))) is a lot clearer than macro-free alternatives. However, when using non-std and avalanche macros, #[component] is unable to track their dependencies correctly, meaning parameters based on those macros may not update correctly. In the future, we may instead opt to consider those macros always updated (at the cost of significantly reduced efficiency), or offer some syntax to specify tracked values explicitly for them. Either way, we recommend you avoid those macros for now.

Rendering dynamic lists

Rendering more heavily dynamic content is where the tracked system shines. But first, a pitfall: here's a potential first attempt at rendering a list of dynamic children:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, state, View};
use avalanche_web::components::{Ul, Li, Text};

#[component]
fn List() -> View {
    let (items, update_items) = state(self, || vec![String::from("a")]);
    Ul(
        self,
        tracked!(items).iter().map(|item| Li(
            self,
            key = item,
            [Text(self, item)]
        )).collect::<Vec<_>>()
    )
}
}

Here, we use the standard iterator methods map and collect to turn a Vec of strings into a Vec of Views.

However, notice that in the component Text(self, item), the implicit text parameter has no tracked! call, so if we later update the element "a", for example, that change won't be appropriately rendered. We can change that with the store hook, which enables storing state with fine-grained tracking. What that means is we can keep track of when specific elements of the state were last updated. This is possible with the Tracked::new method, which allows creating a tracked value with its wrapped value and what render cycle, or Gen, it was created on. The init function and update method of store provide a Gen, allowing us to create and modify tracked values.


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, View};
use avalanche_web::components::{Ul, Li, Text};
use avalanche::{store, Tracked};

#[component]
fn List() -> View {
    let (items, update_items) = store(self, |gen| vec![Tracked::new("a", gen)]);

    Ul(
        self,
        tracked!(items).iter().map(|item| Li(
            self,
            key = tracked!(item),
            [Text(self, tracked!(item))]
        )).collect::<Vec<_>>()
    )
}
}

Now the iter method on items returns elements of type Tracked<&String> rather than &String.

Keys

So far, this explanation has only applied previous concepts, but there's the important new concept of keys. Every avalanche component has a special String key parameter. Whenever a particular component call location in code may be called more than once, it is required to specify a key to disambiguate the multiple instantiations; this is enforced with a runtime panic if not specified. Note that the key must be added on the topmost level, so in our example above,

Li(
    self,
    key = tracked!(item),
    [
        Text(self, tracked!(item))
    ]
)

is good but this is not:

Li(self, [
    Text(
        self,
        key = tracked!(item),
        tracked!(item)
    )
])

Event handling and input elements

So far, we've only used the on_click event, but there are both many more events and more functionality available.

In addition, many events are component-specific, like on_input and on_blur on some form elements. Many of these are available, but some have not been implemented yet. If you're missing one, please file an issue!

Events

Every global event is settable as a parameter on every non-Text avalanche_web component. Each event handling parameter takes a function of type impl Fn(TypedEvent<E, C>) where E is the web_sys type for the handler's event and C is web_sys's native type for the component's native DOM element.

Often, we don't need the event, so we omit it, hence closures like on_click = move |_| ....

Input elements

One case where events are useful is with input elements. That's because the TypedEvent type gives access to the current_target() method, which provides a reference to the component's associated native reference. We can use this to keep track of an Input element's state:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, state, View};
use avalanche_web::components::Input;

#[component]
fn ControlledInput() -> View {
    let (text, set_text) = state(self, || String::new());

    Input(
        self,
        value = tracked!(text).clone(),
        on_input = move |e| set_text.set(e.current_target().unwrap().value())
    )
}
}

In this example, text holds the input's current contents, allowing us to use it for other purposes.

In React, programmers often use on_change instead of on_input, but React semantics do not match native browser ones in this case; use on_input in avalanche_web instead.

For a more complex example of how state and events work in avalanche, check out the todomvc example.

Advanced component props

Documenting parameters

Documentation is important to ensure components are easy to use and understand. Parameters can be documented with standard Rust documentation comments and attributes:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, View};
use avalanche_web::components::Text;

/// Repeats a character for the specified number of times.
#[component]
fn Repeat(
    /// The character to repeat.
    character: char, 
    #[doc = "How many times the character is repeated."]
    n: usize,
) -> View {
    Text(self, tracked!(character).to_string().repeat(tracked!(n)))
}
}

Default parameters

Imagine that we wanted to hide the Repeat component if the user does not supply a value for n. We can do this by using usize's default value:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, View};
use avalanche_web::components::Text;

#[component]
fn Repeat(
    character: char, 
    // How many times the character is repeated.
    #[default]
    n: usize
) -> View {
    Text(self, tracked!(character).to_string().repeat(tracked!(n)))
}
}

Specifying custom default values is also possible. If we want to make Repeat excitement-themed, we could choose a default character value '!':


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, View};
use avalanche_web::components::Text;

#[component]
fn Repeat(
    #[default = '!']
    character: char, 
    #[default]
    // How many times the character is repeated.
    n: usize
) -> View {
    Text(self, tracked!(character).to_string().repeat(tracked!(n)))
}
}

The component can now be called with Repeat(self), which would render nothing, or Repeat(self, count = 3), which would render !!!.

A default value can be any valid Rust expression. The expression will only be evaluated if the caller of the component does not supply their own value.

Generic components

Generic components can be written similarly to generic functions:


#![allow(unused)]
fn main() {
use avalanche::{component, tracked, View};
use avalanche_web::components::Text;
use std::string::ToString;

#[component]
fn SayHello<S: ToString>(name: S) -> View {
    Text(self, format!("Hello, {}!", tracked!(name).to_string()))
}
}

Note that impl Trait properties are not supported in components.