Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Logo

Natrix is a Rust-first frontend framework. Embracing Rust’s strengths—leveraging smart pointers, derive macros, the builder pattern, and other idiomatic Rust features to create a truly native experience.

A Simple Example

A simple counter in Natrix looks like this:

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct Counter(usize);

impl Component for Counter {
    fn render() -> impl Element<Self> {
        e::button()
            .text(|ctx: R<Self>| *ctx.0)
            .on::<events::Click>(|ctx: E<Self>, _, _| {
                *ctx.0 += 1;
            })
    }
}

fn main() {
   natrix::mount(Counter(0));
}

Standout features

  • No macro DSL – Macro-based DSLs break formatting & Rust Analyzer support. Natrix avoids them completely for a smoother dev experience.
  • Derive macros for reactive state – No need for useSignal everywhere, define a struct, thats it.
  • Callbacks use references to state – Instead of closures capturing state setters, Natrix callbacks take a reference to the state, which better aligns with Rust’s ownership model.
  • JS style bundling solution – Natrix has a compile time css and asset bundling solution that works with dependencies out of the box.

Design Goals

  • Developer experience first – Natrix is designed to feel natural for Rust developers.
  • Idiomatic Rust – We use Rust-native features & patterns, not what worked for js.
  • Stop porting JS to Rust – Rust is an amazing language, let’s build a frontend framework that actually feels like Rust.

Getting Started

Installation

The natrix cli requires the following dependencies:

Install the natrix cli with the following command:

cargo install --locked natrix-cli

Creating a new project

To create a new natrix project, run the following command:

natrix new <project-name>

This will by default use nightly rust, if you wish to use stable rust, you can use the --stable flag:

natrix new <project-name> --stable

All features work on stable rust, but nightly includes more optimizations as well as results in smaller binaries. As well as provides some quality of life improvements. see Features for more information.

Running the project

To run the project, navigate to the project directory and run:

natrix dev

This will start a local server that auto reloads on changes. Try changing the text in src/main.rs to see the changes live.

Further Reading

  • Components - Components are the core of natrix, and are the most important part of the framework.
  • Html - This goes over the html_elements module and how to use it.

FAQ

Why is there no html! macro?

Natrix does not use a macro DSL for HTML generation. This is to avoid the issues that come with macro-based DSLs, such as breaking formatting and Rust Analyzer support. Instead, Natrix uses a builder pattern to create HTML elements, which is more idiomatic in Rust and provides a smoother developer experience.

question

Is this a feature you want? Consider making a crate for it! It should be fully possible to create a macro that generates the builder pattern calls.

Why a custom build tool instead of using Trunk

Features such as natrixses unique css bundling require full control of the build process in a way that if we wanted to stick with Trunk would still require us to have a custom tool calling it. By fully taking control of the build process natrix applies the best possible optimizations by default, for example minifying the css and js files, as well as removing dead code from the css. As well as using various cargo and rust build flags to optimize the binary size.

Does natrix handle css for dependencies?

Yes, all css defined in dependencies is bunlded (and DCE-ed!) automatically if you use a dependency from crates.io with no extra setup needed. Similar to what you might be familiar with from component libraries in the js world. This is a unique feature of natrix, as most other rust frameworks require you to manually add the css to your project.

Can you use natrix without #[derive(Component)]?

Not really, the trait the #[derive(Component)] macro implements is #[doc(hidden)] for a reason, and we might make semver-breaking changes to it in non-breaking releases.

Can you use natrix with other frameworks?

In theory you can, but certain features like css and asset bundling will not work as expected. But the core component system should work fine.

features

opt-in features

nightly

this feature enables nightly only features. this includes:

Default types for EmitMessage and ReceiveMessage

this allows you to omit these in your trait implementation, which is really nice.

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct Example;

impl Component for Example{
    fn render() -> impl Element<Self> {
        e::div()
            .text("hello world")
    }
}

must_not_suspend

this annotates certain framework structs as must_not_suspend, which lets rust warn you if you misuse them in async contexts.

important

this requires your project to also enable the feature and use the lint

#![feature(must_not_suspend)]
#![warn(must_not_suspend)]

default_app

This feature flag is enabled by default in the project template. And is a collection of features considered "default" for applications. We opted for this over normal default cargo features because we think it is important for libraries to use the minimal amount of features. Libraries should never enable this feature flag. The intent is that even if a library uses all the below features it should not be tempted to simply use default-features = true.

  • console_log
  • async

console_log

Automatically sets up console_log on mount.

async

Enables the use of ctx.use_async

async_utils

Enables the various async wrappers for browser apis in async_utils

test_utils

Various testing utilities, this should be enabled via a [dev-dependencies].

[dev-dependencies]
natrix = {version = "*", features=["test_utils"]}

Internal features

You might notice a few _internal_* features listed for natrix itself, and you'll also see _natrix_internal_* proxy features in your own crate's Cargo.toml. These are internal features, and as such, we won't be documenting their specific functionalities in detail.

These features are primarily used by natrix-cli to build special versions of your application for bundling reasons, such as CSS extraction or Static Site Generation (SSG). The _natrix_internal_* entries in your Cargo.toml act as "feature proxies," allowing the bundler to correctly apply these configurations during the build process without needing to modify your project's manifest directly.

If you are migrating an existing project to a newer Natrix version, it's recommended to generate a new Natrix project. You can then copy over any new _natrix_internal_* feature proxies from the generated Cargo.toml into your existing project to ensure compatibility with the latest bundler requirements.

Panic Policy

The framework makes liberal use of debug only panics, but is very careful about panics in release! its our goal that any issues should be highlighted in debug builds, but not in release builds. On release builds natrix will silently fail in many cases, this is to ensure that the framework does not panic in production.

When does Natrix panic (in debug builds)?

Very unlikely

  • Js Environment Corruption - If something causes requires javascript methods to be missing, or otherwise fail.
    • In release natrix will skip executing the action it attempted, for example creating a dom node.
  • Unexpected Dom State - If natrix cant find a expected dom node or the node isnt of the expected type.
    • Natrix will skip updating that part of the dom tree

User Errors

  • Internal Borrow Errors - These should only be triggrable by misuse of ctx.deferred_borrow/ctx.use_async.
    • Natrix will skip handling the event/message, this might lead to dropped messages.
  • User Borrow Errors - If you use .borrow_mut while a borrow is active (which again can only happen due to dev error) it will panic in debug builds.
    • In release builds it will return None to signal the calling context should cancel itself.
  • Other Validations A few methods have debug_asserts, listing all of them would be impractical.

When does Natrix panic (in release builds)?

Very unlikely

  • Window or Document Not Found - If the window or document is not found, natrix will panic.
  • Mount Not Found - if mount fails to find the standard natrix mount point it will error.

User Errors

  • User Panics - This one should be obvious.
  • Moving values outside intended scope - Certain values are intended to only be valid in a given scope.
    • Using interior mutability to move a EventToken outside its intended scope will likely lead to bugs if used to call apis in non-event contexts.
    • Using interior mutability to move a Guard, or using it after a .await, will invalidate its guarantees.

What does natrix do in the case of a panic?

Unlike native rust, a panic in wasm does not prevent the program from continuing. This can lead to unexpected behavior if state is left in a invalid state, or worse lead to undefined behavior. Therefor natrix will always do its best to prevent further rust execution after a panic, this is done by checking a panic flag at the start of every event handler, natrix also effectively freezes all async code using a special wrapping future that stops propagation of .poll calls on panic.

Components

Components are important part of natrix, and are the core of the reactivity system.

note

If you are looking for a way to create a component without any state there is a more light weight alternative in the Stateless Components section.

Basic Components

Components are implemented by using the Component derive macro and manually implementing the Component trait. This is because the derive macro actually implements the ComponentBase trait.

Components have 3 required items, render, EmitMessage and ReceiveMessage.

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct HelloWorld;

impl Component for HelloWorld {
    type EmitMessage = NoMessages;
    type ReceiveMessage = NoMessages;
    fn render() -> impl Element<Self> {
        e::div().text("Hello World")
    }
}

fn main() {
    natrix::mount(HelloWorld);
}

important

With the nightly feature you can omit the EmitMessage and ReceiveMessage types, as they default to NoMessages. In all other examples we will omit them for simplicity as nightly is the recommended toolchain.

The render function should return a type that implements the Element trait. This is usually done by using the Html Elements or rust types that implement the Element trait. Elements are generic over the component type, hence impl Element<Self>, this provides strong type guarantees for reactivity and event handlers without needing to capture signals like in other frameworks.

In contrast to frameworks like React, the render function is not called every time the component needs to be updated. Instead, it is only called when the component is mounted. This is because natrix uses a reactivity system that allows fine-grained reactivity.

State

Now components with no state are not very useful (well they are, but you should use Stateless Components instead), so lets add some state to our component. This is done simply by adding fields to the struct.

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct HelloWorld {
    counter: u8,
}

impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::button()
    }
}

fn main() {
    natrix::mount(HelloWorld { counter: 0 });
}

As you can see when mounting a component with state you simply construct the instance without needing any wrappers.

Displaying State

note

The .text method is a alias for .child, so the following section applies to both.

Natrix uses callbacks similar to other frameworks, but instead of capturing signals callbacks instead take a reference to the component. This is mainly done via the R type alias, R<Self> is a alias for &mut RenderCtx<Self>

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct HelloWorld {
    counter: u8,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::button()
            .text(|ctx: R<Self>| *ctx.counter)
    }
}

fn main() {
    natrix::mount(HelloWorld { counter: 0 });
}

We need to specify the argument type of the closure, this is because of limitation in the type inference system. The closures also can return anything that implements the Element trait, so you can use any of the Html Elements or any other type that implements the Element trait.

tip

See the reactivity section for more information on how fine grained reactivity works and best practices.

Updating State

Updating state is done very similarly, but using E, the .on method takes a callback that is called when the event is triggered. The callback takes a reference to the component and the event as arguments, as well as a EventToken which is used to access event-only apis. The event is passed as a generic type, so you can use any event that implements the Event trait. the second argument will automatically be inferred to the type of the event. for example the Click event will be passed as a MouseEvent type.

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct HelloWorld {
    counter: u8,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::button()
            .text(|ctx: R<Self>| *ctx.counter)
            .on::<events::Click>(|ctx: E<Self>, _, _| {
                *ctx.counter += 1;
            })
    }
}

fn main() {
    natrix::mount(HelloWorld { counter: 0 });
}

important

causing a re-entry into the natrix code via js, from within a event handler, for example calling .click() on a button with a natrix handler while already having access to a ctx, especially if on the same component, will cause state desyncs (and panics on debug mode).

Defining methods

Construction

Construction methods can simple be defined as normal

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
pub struct MyComponent {
    private_field: u8,
}

impl MyComponent {
    pub fn new(initial_value: u8) -> Self {
        Self { private_field: initial_value }
    }
}

impl Component for MyComponent {
    fn render() -> impl Element<Self> {
        e::div()
    }
}

fn main() {
    natrix::mount(MyComponent::new(0));
}

Methods for ctx

The above wont let you define methods that work on ctx, this is because ctx is actually a different type constructed by the derive macro. This type can be gotten using the natrix::data macro, like the following:

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct HelloWorld {
    counter: u8,
}

impl natrix::data!(HelloWorld) {
    pub fn increment(&mut self) {
        *self.counter += 1;
    }
}

State-Less Components

Stateless components are, technically speaking, not even a explicit feature of natrix. But just a by product of the design. They are simply functions (or methods, or anything else) that returns impl Element<...>, this chapther is here to outline what they usually look like and some common patterns. And is not a exhustive list of whats possible with the amazing work of art thats rust trait system.

As mentioned in the component chaphter Element is generic over the component state it references, this is true even if it doesnt reference any state. This means that your stateless component functions needs to be generic:

extern crate natrix;
use natrix::prelude::*;

fn hello<C: Component>() -> impl Element<C> {
    e::h1().text("Hello World")
}

These can then be called from within a component, or even from other stateless components:

extern crate natrix;
use natrix::prelude::*;

fn hello<C: Component>() -> impl Element<C> {
    e::h1().text("Hello World")
}
#[derive(Component)]
struct HelloWorld;
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::div()
            .text("Hello World")
            .child(hello())
    }
}

Passing arguments

Since they are just functions stateless functions can take arguments like any other function.

extern crate natrix;
use natrix::prelude::*;
fn hello<C: Component>(name: String) -> impl Element<C> {
    e::h1().text("Hello ").text(name)
}

Now this has a downside, imagine if we wanted name to be reactive? we would have to put the entire component call in the reactive closure.

extern crate natrix;
use natrix::prelude::*;

fn hello<C: Component>(name: String) -> impl Element<C> {
    e::h1().text("Hello ").text(name.clone())
}
#[derive(Component)]
struct HelloWorld {
    name: String,
}

impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::div()
            .text("Hello World")
            .child(|ctx: R<Self>| hello(ctx.name.clone()))
    }
}

But what if hello was actually really complex? We would be recreating that entire dom tree every time name changes. The solution is to actually make hello generic over the hello argument!

extern crate natrix;
use natrix::prelude::*;
fn hello<C: Component>(name: impl Element<C>) -> impl Element<C> {
    e::h1().text("Hello ").child(name)
}

Now we can make just the name part reactive

extern crate natrix;
use natrix::prelude::*;

fn hello<C: Component>(name: impl Element<C>) -> impl Element<C> {
    e::h1().text("Hello ").text(name)
}
#[derive(Component)]
struct HelloWorld {
    name: String,
}

impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::div()
            .text("Hello World")
            .child(hello(|ctx: R<Self>| ctx.name.clone()))
    }
}

And now only the name part will be recreated when name changes.

Events

Natrix provides the EventHandler trait which makes taking event handlers in stateless components easier, in reality this trait is only implement for closures of the appropriate signature.

extern crate natrix;
use natrix::prelude::*;
use natrix::dom::EventHandler;

fn fancy_button<C: Component>(
    on_click: impl EventHandler<C, events::Click>,
) -> impl Element<C> {
    e::button()
        .text("Click me!")
        .on(on_click)
}

This can be used like this:

extern crate natrix;
use natrix::prelude::*;
use natrix::dom::EventHandler;
fn fancy_button<C: Component>(
    on_click: impl EventHandler<C, events::Click>,
) -> impl Element<C> {
    e::button()
        .text("Click me!")
        .on(on_click)
}

#[derive(Component)]
struct HelloWorld {
    counter: u8,
};

impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::div()
            .child(fancy_button(|ctx: E<Self>, _, _| {
                *ctx.counter += 1;
            }))
    }
}

Concrete stateless components

Yet again this isnt a explicit feature, but rather "common sense". You can also write helper functions that dont use generics, and hence can declare their closures directly.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct Counter {
    value: i16,
}

fn change_button(delta: i16) -> impl Element<Counter> {
    e::button()
        .text(delta)
        .on::<events::Click>(move |ctx: E<Counter>, _, _| {
            *ctx.value += delta;
        })
}

impl Component for Counter {
    fn render() -> impl Element<Self> {
        e::div()
            .child(change_button(-10))
            .child(change_button(-1))
            .text(|ctx: R<Self>| *ctx.value)
            .child(change_button(1))
            .child(change_button(10))
    }
}

Non event handling closures.

Also nothing stopping you from taking explicit closures.

extern crate natrix;
use natrix::prelude::*;
fn change_button<C: Component>(
    delta: i16,
    modify: impl Fn(E<C>, i16) + 'static,
) -> impl Element<C> {
    e::button()
        .text(delta)
        .on::<events::Click>(move |ctx: E<C>, _, _| modify(ctx, delta))
}

fn change_buttons<C: Component>(
    modify: impl Fn(E<C>, i16) + Clone + 'static
) -> impl Element<C> {
    e::div()
        .child(change_button(-10, modify.clone()))
        .child(change_button(-1, modify.clone()))
        .child(change_button(1, modify.clone()))
        .child(change_button(10, modify.clone()))
}

#[derive(Component)]
struct Counter {
    value: i16,
}

impl Component for Counter {
    fn render() -> impl Element<Self> {
        e::div()
            .text(|ctx: R<Self>| *ctx.value)
            .child(change_buttons(|ctx: E<Self>, delta| {*ctx.value += delta;}))
    }
}

Sub Components

Components wouldnt be very useful if we could not compose them. Due to trait limitations we cant use components as Elements directly. But there is a simple SubComponent wrapper to facilitate this.

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct MyChild {
    /* Any State You Want */
}

impl Component for MyChild {
    fn render() -> impl Element<Self> {
        /* Your Component */
        e::div()
    }
}

#[derive(Component)]
struct MyParent {
    /* Any State You Want */
}

impl Component for MyParent {
    fn render() -> impl Element<Self> {
        e::div()
            .child(SubComponent::new(MyChild {
                /* Initial Child State */
            }))
    }
}

Message Passing

A common requirement is communication between components. This is where the EmitMessage and ReceiveMessage associated types come in. These are used to declare what type is used for message passing to and from the component. The NoMessages type is a enum with no variants (i.e similar to Infallible) and is used when you do not need to pass messages.

Child to Parent

Define the EmitMessage type to the type of the message you will be emitting and then use ctx.emit, you can then use .on to listen for the message in the parent component.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct MyChild;

impl Component for MyChild {
    type EmitMessage = usize;

    fn render() -> impl Element<Self> {
        e::button()
            .text("Click Me")
            .on::<events::Click>(|ctx: E<Self>, token, _| {
                ctx.emit(10, token);
            })
    }
}

#[derive(Component)]
struct MyParent {
    state: usize,
};

impl Component for MyParent {
    fn render() -> impl Element<Self> {
        e::div()
            .child(SubComponent::new(MyChild).on(|ctx: E<Self>, msg, _| {
                *ctx.state += msg;
            }))
    }
}

Parent to Child

Similaryly you can use ReceiveMessage to listen for messages from the parent component. You overwrite the default handle_message method to handle the message. In the parent you use .sender to get a sender for the child component.

extern crate natrix;
use natrix::prelude::*;
use natrix::reactivity::state::EventToken;

#[derive(Component, Default)]
struct MyChild {
    state: usize,
}

impl Component for MyChild {
    type ReceiveMessage = usize;

    fn render() -> impl Element<Self> {
        e::div()
            .text(|ctx: R<Self>| *ctx.state)
    }

    fn handle_message(ctx: E<Self>, msg: Self::ReceiveMessage, token: EventToken) {
        *ctx.state += msg;
    }
}

#[derive(Component)]
struct MyParent;

impl Component for MyParent {
    fn render() -> impl Element<Self> {
        let child = SubComponent::new(MyChild::default());
        let sender = child.sender();

        e::div()
            .child(child)
            // We use `move` to move ownership of the sender into the closure
            .on::<events::Click>(move |ctx: E<Self>, token, _| {
                sender.send(10, token);
            })
    }
}

As you see this generally requires you to use a let binding to split the return of .sender. The Sender is also cloneable.

When do messages get processed?

Messages are process right away if possible, if the target is already borrowed (for example if a message is sent in a message handler) the message will be deferred until that components update step.

Generic Components

Components can be generic just the way you would expect.

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct MyComponent<T>(T, T);

impl<T: Eq + 'static> Component for MyComponent<T> {
    fn render() -> impl Element<Self> {
        e::div()
            .text(|ctx: R<Self>| {
                if *ctx.0 == *ctx.1 {
                    "Equal"
                } else {
                    "Not Equal"
                }
            })
    }
}

Generic over Element/ToAttribute

If you want to be generic over something with a Element bound you will run into a recursion error in the type checker.

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct MyComponent<T>(T);

impl<T: Element<Self> + Clone> Component for MyComponent<T> {
    fn render() -> impl Element<Self> {
        e::div()
            .child(|ctx: R<Self>| ctx.0.clone())
    }
}

fn main() {
    mount(MyComponent("Hello World".to_string()));
}

The problem here is that Element needs to be generic over the component, so Element<Self>, but its also enforces a Component bound on its generic, this means that in order to prove MyComponent<T> implements Component it must first prove MyComponent<T> implements Component, which rust doesnt like and errors out on. To solve this you can use the NonReactive wrapper which will allow you to use Element<()> as the generic bound. As the name implies this essentially means that part of the dom tree cant be reactive.

NonReactive is essentially a wrapper that swaps out the component instance its given with ().

extern crate natrix;
use natrix::prelude::*;
use natrix::reactivity::NonReactive;

#[derive(Component)]
struct MyComponent<T>(T);

impl<T: Element<()> + Clone> Component for MyComponent<T> {
    fn render() -> impl Element<Self> {
        e::div()
            .child(|ctx: R<Self>| NonReactive(ctx.0.clone()))
    }
}

fn main() {
    natrix::mount(MyComponent("Hello World".to_string()));
}

NonReactive also implements ToAttribute so a similar trick can be used for it.

Async

Async is a really important part of any web application, as its how you do IO and talk to other services or your backend. Natrix provides DeferredCtx, via the .deferred_borrow method, to facilitate this. as well as the .use_async helper.

What is a DeferredCtx?

Internally natrix stores the state as a Rc<RefCell<...>>, DeferredCtx is a wrapper around a Weak<...> version of the same state, that exposes a limited safe api to allow you to borrow the state at arbitrary points in the code, usually in async functions.

The main method on a deferred context is the .borrow_mut method, which allows you to borrow the state mutably. This returns a Option<DeferredRef> which internally holds both a strong Rc and a RefMut into the state. If this returns None, then the component is dropped and you should in most case return/cancel the current task.

important

Holding a DeferredRef across a yield point (holding across .await) is considered a bug, and will likely lead to a panic on debug builds, and desynced state on release builds.

On borrowing (via .borrow_mut) the framework will clear the reactive state of signals, and will trigger a reactive update on drop (i.e the framework will keep the UI in sync with changes made via this borrow). But this also means you should not borrow this in a loop, and should prefer to borrow it for the maximum amount of time that doesnt hold it across a yield point.

Bad Example

extern crate natrix;
use natrix::prelude::*;
use natrix::reactivity::state::DeferredCtx;

async fn foo() {}

#[derive(Component)]
 struct HelloWorld {
     counter: u8,
}

impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::div()
    }
}

async fn use_context(mut ctx: DeferredCtx<HelloWorld>) {
    let mut borrow = ctx.borrow_mut().unwrap(); // Bad, we are panicking instead of returning.
    *borrow.counter += 1;

    drop(borrow); // Bad we are triggering multiple updates.
    let mut borrow = ctx.borrow_mut().unwrap();

    *borrow.counter += 1;
    foo().await; // Bad we are holding the borrow across a yield point.
    *borrow.counter += 1;
}

Good Example

extern crate natrix;
use natrix::prelude::*;
use natrix::reactivity::state::DeferredCtx;

async fn foo() {}

#[derive(Component)]
 struct HelloWorld {
     counter: u8,
}

impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::div()
    }
}

async fn use_context(mut ctx: DeferredCtx<HelloWorld>) {
    { // Scope the borrow
        let Some(mut borrow) = ctx.borrow_mut() else {
            return;
        };
        *borrow.counter += 1;
        *borrow.counter += 1;
    } // Borrow is dropped here, triggering a reactive update.

    foo().await;

    let Some(mut borrow) = ctx.borrow_mut() else {
        return;
    };
    *borrow.counter += 1;
}

In other words, you should consider .borrow_mut to be a similar to Mutex::lock in terms of scoping and usage. You should not hold the borrow across a yield point, and you should not hold it for longer than necessary.

.use_async

In most cases where you have use for a DeferredCtx it will be in a async function. The .use_async method is a wrapper that takes a async closure and schedules it to run with a DeferredCtx borrowed from the state. The closure should return Option<()>, This is to allow use of ? to return early if the component is dropped.

extern crate natrix;
use natrix::prelude::*;

async fn foo() {}

#[derive(Component)]
struct HelloWorld {
    counter: u8,
}

impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::button()
            .text(|ctx: R<Self>| *ctx.counter)
            .on::<events::Click>(|ctx: E<Self>, token, _| {
                ctx.use_async(token, async |ctx| {
                    {
                        let mut borrow = ctx.borrow_mut()?;
                        *borrow.counter += 1;
                    }

                    foo().await;
                    let mut borrow = ctx.borrow_mut()?;
                    *borrow.counter += 1;

                    Some(())
                });
            })
    }
}

Html

Html elements are the building blocks of web pages. While other rust frameworks aim for a JSX-like syntax, this library uses a more traditional approach. The goal is to provide a simple and efficient way to create HTML elements without the need for complex syntax, we use the idomatic rust builder pattern.

Natrix uses a single HtmlElement struct to represent all HTML elements. But exposes helper functions for each tag. These are found along side the HtmlElement struct in the html_elements module. Which will most commonly be used via the e alias in the prelude module.

extern crate natrix;
use natrix::prelude::*;
let _: e::HtmlElement<(), _> =
e::div()
;

If you need to construct a element with a tag not found in the library you can use HtmlElement::new.

extern crate natrix;
use natrix::prelude::*;
let _: e::HtmlElement<(), ()> =
e::HtmlElement::new("custom_tag")
;

Children

Children are added using the .child method. This method takes a single child element and adds it to the parent element.

extern crate natrix;
use natrix::prelude::*;
let _: e::HtmlElement<(), _> =
e::div()
    .child(e::button())
    .child(e::h1().child("Hello World!"))
;

tip

the .text method is a alias for .child

Child elements can be any type that implements the Element trait, including other HtmlElement instances, and stdlib types like String, &str, i32, as well as containers such as Option and Result.

Child elements can also be reactive as closures implement the Element trait.

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct MyComponent {
    pub is_active: bool,
}

impl Component for MyComponent {
    fn render() -> impl Element<Self> {
e::div()
    .child(e::button()
        .text("Click me!")
        .on::<events::Click>(|ctx: E<Self>, _, _| {
            *ctx.is_active = !*ctx.is_active;
        })
    )
    .child(|ctx: R<Self>| {
        if *ctx.is_active {
            Some(e::p().text("Active!"))
        } else {
            None
        }
    })
    }
}

format_elements

You can use the format_elements macro to get format! like ergonomics for elements.

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct MyComponent {
    pub counter: u8,
    pub target: u8,
}

impl Component for MyComponent {
    fn render() -> impl Element<Self> {
e::h1().children(
    natrix::format_elements!(|ctx: R<Self>| "Counter is {}, just {} clicks left!", *ctx.counter, *ctx.target - *ctx.counter)
)
    }
}

Which expands to effectively:

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct MyComponent {
    pub counter: u8,
    pub target: u8,
}

impl Component for MyComponent {
    fn render() -> impl Element<Self> {
e::h1()
    .text("Counter is ")
    .child(|ctx: R<Self>| *ctx.counter)
    .text(", just ")
    .child(|ctx: R<Self>| *ctx.target - *ctx.counter)
    .text(" clicks left!")
  }
}

I.e this is much more performant than format! for multiple reasons:

  • You avoid the format machinery overhead.
  • You get fine-grained reactivty for specific parts of the text.

Attributes

Attributes are set using the .attr method. This method takes a key and a value, and sets the attribute on the element.

extern crate natrix;
use natrix::prelude::*;
let _: e::HtmlElement<(), _> =
e::div()
    .attr("data-foo", "bar")
    .attr("data-baz", "qux")
;

Most standard html attributes have type-safe helper functions, for example id, class, href, src, etc. For non-global attributes natrix only exposes them on the supporting elements.

extern crate natrix;
use natrix::prelude::*;
use natrix::dom::attributes;

let _: e::HtmlElement<(), _> =
e::a()
    .href("https://example.com")
    .target(attributes::Target::NewTab) // _blank
    .rel(vec![attributes::Rel::NoOpener, attributes::Rel::NoReferrer])
;

But the following wont compile:

extern crate natrix;
use natrix::prelude::*;
let _: e::HtmlElement<(), _> =
e::div()
    .target("_blank") // error: no method named `target` found for struct `HtmlElement<_, _div>`
;

Attributes can be set by anything that implements the ToAttribute trait, this includes numberics, Option, and bool, and others. Attributes can also be reactive as closures implement the ToAttribute trait.

extern crate natrix;
use natrix::prelude::*;

#[derive(Component)]
struct MyComponent {
    pub is_active: bool,
}

impl Component for MyComponent {
    fn render() -> impl Element<Self> {
e::button()
    .disabled(|ctx: R<Self>| !*ctx.is_active)
    .text("Click me!")
    .on::<events::Click>(|ctx: E<Self>, _, _| {
        *ctx.is_active = !*ctx.is_active;
    })
    }
}

Importantly for the attribute helpers AttributeKind determines what kind of values are allowed for that helper. Important a attribute kind of for example bool also supports Option<bool>, a closure returning bool, etc. For example this wont compile:

extern crate natrix;
use natrix::prelude::*;
let _: e::HtmlElement<(), _> =
e::a()
    .target("_blank") // error: expected `attributes::Target`, found `&'static str`
;

Classes

The .class method is not a alias for .attr, it will add the class to the element, and not replace it. This is because the class attribute is a special case in HTML, and is used to apply CSS styles to elements. The .class method will add the class to the element, and not replace any existing ones.

extern crate natrix;
use natrix::prelude::*;

const FOO: Class = natrix::class!(); // unique class name
const BAR: Class = natrix::class!();

let _: e::HtmlElement<(), _> =
e::div()
    .class(FOO)
    .class(BAR)
;

Classes can also be reactive as closures implement the ToClass trait.

extern crate natrix;
use natrix::prelude::*;

const ACTIVE: Class = natrix::class!();

#[derive(Component)]
struct MyComponent {
    pub is_active: bool,
}

impl Component for MyComponent {
    fn render() -> impl Element<Self> {
e::div()
    .class(|ctx: R<Self>| {
        if *ctx.is_active {
            Some(ACTIVE)
        } else {
            None
        }
    })
    .child(e::button()
        .text("Click me!")
        .on::<events::Click>(|ctx: E<Self>, _, _| {
            *ctx.is_active = !*ctx.is_active;
        })
    )
}
}

Css

note

Natrixses css bundlinging system requires the use of the natrix cli. As such css bundling will not work when embedding natrix in other frameworks.

Natrix uses a unique css bundling system that allows for css to be declared in rust files, but bundled at compile time. This is very different from other rust frameworks, which either do runtime injection, or require static external css files. Both of which have downsides that natrix solves.

The main advantage of this design is that css for dependencies is bundled along with the code on crates.io and is automatically combined with your own at compile time.

Assets

important

Similar to CSS, assets do not work without the natrix cli as a bundler.

Assets can be bundled with the asset! macro, it will include the given file path (relative to the crates Cargo.toml), the macro expands to the runtime path of the asset (prefixed with /, or base_path if set)

extern crate natrix;
use natrix::prelude::*;
let _: e::HtmlElement<(), _> =
e::img()
    .src(natrix::asset!("./assets/my_img.png"))
;

This will include /path/to/crate/./assets/my_img.png in the dist folder, and expand to something like this in rust:

extern crate natrix;
use natrix::prelude::*;
let _: e::HtmlElement<(), _> =
e::img()
    .src("/SOME_HASH-my_img.png")
;

tip

The dev server actually serves the assets from their source paths, so you dont have to worry about the files being copied on every reload.

Reactivity

Callbacks

You have already seen |ctx: R<Self>| ... used in the varying examples in the book. Lets go into some more detail about what this does.

tip

If you're looking to create components without state that still leverage natrix's reactivity system, see the Stateless Components documentation.

The callbacks return a value that implements Element, internally the framework will register which fields you accessed. And when those fields change, the framework will recall the callback and update the element with the result.

Natrix does not use a virtual dom, meaning when a callback is re-called the framework will swap out the entire associated element. See below for tools to mitigate this.

important

Natrix assumes render callbacks are pure, meaning they do not have side effects. If you use stuff like interior mutability it will break framework assumptions, and will likely lead to panics or desynced state. For example using interior mutability to hold onto a Guard outside its intended scope will invalidate its guarantees.

Execution Guarantees

Natrix only makes the following guarantees about when a callback will be called:

  • It will not be called if a parent is dirty.

Thats, natrix does not make any guarantees about the order of sibling callbacks. Natrix guarantees around how often a value is called is... complex because of features such as .watch, in general the reactive features below should be mainly treated as very strong hints to the framework, and optimizations might cause various use cases to result in more or less calls.

Returning different kinds of elements.

Sometimes two branches returns different kinds of elements, this can be solved using Result, or by pre-rendering them using .render. Which produces the internal result of a element render (which itself implements Element for this exact purpose)

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    counter: u8,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        if *ctx.counter > 10 {
            e::h1().text("Such big").render()
        } else {
            "Oh no such small".render()
        }
    })
     }
}

tip

For handling multiple types of html elements, theres .generic(), which returns HtmlElement<C, ()>, i.e erases the dom tag, allowing you to for example construct different tags in a if, and then later call methods on it. Ofc doing this means only global attribute helpers can be used, but you can always use .attr directly.

.watch

Now imagine you only access part of a field.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    counter: u8,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        format!("{}", *ctx.counter > 10)
    })
     }
}

This will work, but it will cause the callback to be called every time counter changes, even if it causes no change to the dom. In this case thats fine, its not a expensive update, but imagine if this was a expensive operation. This is where .watch comes in.

What it does is cache the result of a callback, and then it calls it on any change, but will compare the new value to the old value. And only re-runs the surrounding callback if the value has changed.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    counter: u8,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        let bigger_than_10 = ctx.watch(|ctx| *ctx.counter > 10);
        format!("{}", bigger_than_10)
    })
     }
}
Actionctx.watch(...) runsformat!(...) runs
initial renderyesyes
0 -> 1yesno
10 -> 11yesyes
11 -> 12yesno
11 -> 10yesyes

This can be more usefully used for example when dealing with a Vec of items. For example ctx.watch(|ctx| ctx.items[2])

Nested closures

In a similar vein to watch, you can return a closure from a closure, which basically constructs separate reactive barriers for the same output.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    show: bool,
    counter: u8,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        if *ctx.show {
            (|ctx: R<Self>| *ctx.counter).render()
        } else {
            e::div().text("Nothing to see").render()
        }
    })
     }
}

Here both ctx.show and ctx.counter are used to render the same dom node, but when ctx.counter changes only that inner closure re-runs. Now the example above might not be a good usecase, it involves a extra reactive hook allocation, and all the memory footprint that comes with that. All to save a single boolean check, likely not worth it. But for more complex surrounding logic it might make sense to do.

guard_...

Problem

Now you could imagine .watch being useful for Option and Result types. It is, but you have to be careful about how you use it.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    option: Option<u8>,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        if ctx.watch(|ctx| ctx.option.is_some()) {
            let value = ctx.option.unwrap();
            e::h1()
                .text(format!("Value: {}", value))
                .render()
        } else {
            "None".render()
        }
    })
     }
}

This will work, but it will still cause the callback to be called every time option changes because it still uses ctx.option directly. As with any fine-grained reactivity, you can use nested callbacks to get a better effect.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    option: Option<u8>,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        if ctx.watch(|ctx| ctx.option.is_some()) {
            e::h1()
                .text(|ctx: R<Self>| ctx.option.unwrap())
                .render()
        } else {
            "None".render()
        }
    })
     }
}

And this does work exactly as we want it to. But there is one downside. That .unwrap, we know for a fact that it will never panic because of the condition above. But because in rusts eyes these callbacks are not isolated we cant prove to it otherwise. In addition that unwrap means you cant use the recommended lint to deny them.

Solution

This is where the guard_option (and guard_result) macros come in. They use ctx.watch internally, and gives you a way to access the value without having to unwrap it.

extern crate natrix;
use natrix::prelude::*;
use natrix::guard_option;

#[derive(Component)]
struct HelloWorld {
    option: Option<u8>,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        if let Some(guard) = guard_option!(|ctx| ctx.option.as_ref()) {
            e::h1()
                .text(move |ctx: R<Self>| *ctx.get(&guard))
                .render()
        } else {
            "None".render()
        }
    })
     }
}

This will work exactly the same as the previous example, but hides the .unwrap() from the user.

warning

Similarly to DeferredRef you should not hold this across a yield point. guard_option does in fact still use .unwrap() internally, meaning its effectively the same as the "bad" code above. It is simply a nice api that enforces the invariant that you only .unwrap in a context where you have done the .is_some() check in a parent hook.

List

You often have to render a list of items, and doing that in a reactive way is a bit tricky. The List element is a way to do this.

extern crate natrix;
use natrix::prelude::*;
use natrix::reactivity::State;
use natrix::dom::List;

#[derive(Component)]
struct HelloWorld {
    items: Vec<u8>,
}

impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::div()
            .child(List::new(
            |ctx: &State<Self>| &ctx.items,
            |_ctx: R<Self>, getter| {
                e::div().text(move |ctx: R<Self>| getter.get_watched(ctx))
            }
        ))
    }
}

See the docs in the List module for more details.

Debugging

Debuggers

When using the dev profile natrix will include both DWARF debugging information and a inline sourcemap.

As of writing firefox does not support DWARF based debugging, and in our experience doesnt support wasm breakpoints (including sourcemap backed ones). Hence we recommend you use chromeium for debugging your applications.

tip

Chromeium works more than well enough with sourcemap only, but for even better debugging support you can install the DWARF extension

Logging

And ofc a debugging section wont be complete without print-debugging. The default features, specifically console_log, setup the log crate to log to the browser console automatically. you might have already seen some of its output if you open the console in a dev build. You can naturally use the log crate yourself to log various information for debugging purposes.

important

The default project template sets the log level for dev builds to info, you can change this in your Cargo.toml

Testing

Testing is a important part of any project. Natrix doesnt have a dedicated testing framework, instead we recommend you use wasm-pack to run your tests. But natrix does provide the test_utils module to help with testing, which is enabled with the test_utils feature flag.

The primary functions are mount_test and get.

Example

extern crate natrix;
extern crate wasm_bindgen_test;
use natrix::prelude::*;

const HELLO: Id = natrix::id!();

#[derive(Component)]
struct HelloWorld;

impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
        e::div()
            .text("Hello World")
            .id(HELLO)
    }
}

mod tests {
    use super::*;
    use natrix::test_utils;
    use wasm_bindgen_test::wasm_bindgen_test;

    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);

    #[wasm_bindgen_test]
    fn test_hello_world() {
        test_utils::mount_test(HelloWorld);
        let hello = test_utils::get(HELLO);
        assert_eq!(hello.text_content(), Some("Hello World".to_string()));
    }
}

fn main() {}

This will mount the HelloWorld component and then check if the text content of the element with id HELLO is "Hello World". This is a simple test, but it shows how to use the test_utils module to test your components. These tests can be run as follows:

wasm-pack test --headless --chrome --firefox

note

From out experience the firefox webdriver is very slow to spin up, and even fails at semmingly random times.

CLI and Configuration

Natrix comes with a powerful CLI that helps you create, develop, and build your applications. This page explains all the available commands and configuration options.

The Natrix CLI

The Natrix CLI is a set of tools designed to make working with Natrix projects as smooth as possible. It handles everything from project creation to development servers with hot reloading and optimized production builds.

Creating New Projects

The new command creates a brand new Natrix project with all the necessary files and configuration set up for you.

natrix new my-awesome-app

tip

By default, new projects use nightly Rust for better optimizations and smaller binaries. If you prefer to use stable Rust instead, add the --stable flag:

natrix new my-awesome-app --stable

This command creates a new project directory with the following structure:

my-awesome-app/
├── Cargo.toml
├── .gitignore
├── rust-toolchain.toml
└── src/
    └── main.rs

Development Server

The dev command starts a local development server with live reloading.

natrix dev

Building for Production

When you're ready to deploy your app, use the build command to create an optimized production build.

natrix build

Configuration

Natrix can be configured through your project's Cargo.toml file. Add a [package.metadata.natrix] section to customize how Natrix builds your application.

[!IMPORTANT] These options only take affect for production builds. For dev all these settings have sensible defaults.

Cache Busting

Control how asset URLs are versioned to ensure browsers load the latest versions:

[package.metadata.natrix]
cache_bust = "content"  # Options: "none", "content", "timestamp"
  • content: (Default) Creates a hash based on the file content
  • timestamp: Creates a hash based on the current build time
  • none: Doesn't add any cache busting

tip

Content-based cache busting is recommended for production as it only changes URLs when the content actually changes, maximizing cache efficiency.

Base Path

If your app isn't hosted at the root of a domain, you can specify a base path prefix:

[package.metadata.natrix]
base_path = "/my-app" 

This configures all asset URLs to be prefixed with the specified path.

important

Always include a leading slash in your base_path value.

SSG

By default natrix extracts metadata from your application, importantly for this to work your application must call mount, and should not access any browser apis before or after it. If your application does not use mount you should set this option to false.

This will force css to be injected at runtime instead, and more importantly will not attempt to build and call your binary during bundling.

[package.metadata.natrix]
ssg = false # Default: True

for example if you are doing something like this you need to set ssg = false

fn main() {
    let document = web_sys::window().unwrap(); // This will fail during ssg
    // ...
}

Deployment

To deploy a natrix application simple use natrix build and upload the dist folder to your server. The dist folder contains all the files needed to run your application.

Library

Libraries can be published to crates.io and be used in other projects without having to explicitly bundle css or assets.

important

Make sure to depend on natrix without default features, as some default features are intended to be disableable from end user applications.

Usage in other frameworks

The mount_at function can be used to mount a natrix component at a custom location. This function will return a RenderResult that should be kept alive until the component is unmounted. And ideally dropped when the component is unmounted.

important

Features that depend on the natrix build pipeline will not work unless the application is built with natrix build. If you do not wish to build the final application with natrix, you can use the natrix build command to build the application and then copy files such as styles.css from natrixses dist folder to your application.