Introduction

Logo

Natrix is a Rust-first frontend framework. Where other frameworks aim to bring React-style development to rust, Natrix embraces 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() {
   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, state is directly tied to Rust’s type system.
  • 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.
  • Fine-grained reactivity – Natrix only updates what's necessary, minimizing re-renders and maximizing performance.
  • Borrow Safety - We only use one RefCell per component instance, all other state is managed within the rust borrowing rules, which gives stronger guarantees of no panics as well as performance.
  • No Panic Policy - Natrix has a naunced panic policy, see the Panic Policy chapter for more information.

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.

In addition a interesting thing natrix build does is inspect the enabled features and change its compilation automatically, for example if the panic_hook feature is disabled it will compile with aggressive DCE that eliminates all panic code, including branches that lead to panics.

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 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)]

async_utils

adds the async_utils module which contains stuff like a wasm compatible sleep function.

extern crate natrix;
use std::time::Duration;
async fn foo() {
    natrix::async_utils::sleep(Duration::from_secs(1)).await;
}

ergonomic_ops

Implements AddAssign, SubAssign, etc on signals, allowing you to omit the dereference in certain situations. This is disabled by default because it does not match other smart pointers, and still requires the dereference in certain situations.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct Hello { counter: u8 }
impl Component for Hello {
    fn render() -> impl Element<Self> {
       e::button().on::<events::Click>(|ctx: E<Self>, _|{
// Without `ergonomic_ops`
*ctx.counter += 1;
*ctx.counter = *ctx.counter + 1;

// With `ergonomic_ops`
ctx.counter += 1;
*ctx.counter = *ctx.counter + 1;
})
}}

Notice how we still need the dereference for a plain assignment and addition? This inconsistency is why this feature is disabled by default as many might find this confusing.

either

Implements Component and ToAttribute for Either from the either crate.

default features

panic_hook

This feature enables a panic hook that is auto installed when using mount (or can be set manually with natrix::set_panic_hook), this panic hook will prevent any further rust code from running if a panic happens, which prevents undefined behaviour.

On the default natrix new project (on nightly), a normal build is 30KB while a build without this feature is 22KB.

danger

Disabling this should be considered unsafe, and is an assertion from you that your code will never panic.

This will actually make natrix build strip out all branches that panic, which means hitting those branches is undefined behaviour.

auto nightly

natrix will auto detect when its compiled on nightly and use certain (non-public-facing) features. this is one of the reasons its recommended to use nightly rust.

  • optimize text updates, on stable updating a text node is done via replace_child, on nightly it uses set_text_content

Panic Policy

The framework only makes use of debug_assert!, 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)?

  • 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
  • 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.

When does Natrix panic (in release builds)?

  • Mount Not Found - if mount fails to find the standard natrix mount point it will error.
  • User Panics - This one should be obvious.
  • Misused Guards - If you use async or interor mutability to use a Guard outside of the context it was created in you are violating its contract, which might lead to panics.
  • Deferred Borrows After Panic - If you use .borrow_mut after a panic has happened it will cause another panic, as returning to the user code could cause undefined behaviour.

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() {
    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() {
    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() {
    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. 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() {
    mount(HelloWorld { counter: 0 });
}

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() {
    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.

failure

This feature isnt implemented yet

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.

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::*;
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::*;
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;
            }))
    }
}

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 C 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(C::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>, _| {
                ctx.emit(10);
            })
    }
}

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

impl Component for MyParent {
    fn render() -> impl Element<Self> {
        e::div()
            .child(C::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::*;
#[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) {
        *ctx.state += msg;
    }
}

#[derive(Component)]
struct MyParent;

impl Component for MyParent {
    fn render() -> impl Element<Self> {
        let (child, sender) = C::new(MyChild::default()).sender();
        e::div()
            .child(child)
            // We use `move` to move ownership of the sender into the closure
            .on::<events::Click>(move |ctx: E<Self>, _| {
                sender.send(10);
            })
    }
}

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 passing uses async channels internally, this means the messages will be processed once the current components reactivity cycle is finished. This will still run before the next reflow of the browser, and all messages are batched for efficiency.

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::component::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() {
    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::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::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>, _| {
                ctx.use_async(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()
    .class("my-component")
    .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
        }
    })
    }
}

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 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::*;
let _: e::HtmlElement<(), _> =
e::a()
    .href("https://example.com")
    .target("_blank")
    .rel("noopener 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()
    .class("my-button")
    .disabled(|ctx: R<Self>| !*ctx.is_active)
    .text("Click me!")
    .on::<events::Click>(|ctx: E<Self>, _| {
        *ctx.is_active = !*ctx.is_active;
    })
    }
}

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::*;
let _: e::HtmlElement<(), _> =
e::div()
    .class("foo")
    .class("bar")
    .class("baz")
;

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.

Global css

Global css is emitted using the global_css! macro, which takes a string literal.

extern crate natrix;
use natrix::prelude::*;
global_css!("
    body {
        background-color: red;
    }
");

important

Due to css-tree shaking, dynamic classes might be stripped from the final css bundle. To avoid this use the custom @keep directive.

extern crate natrix;
use natrix::prelude::*;
global_css!("
    @keep dynamically_generated_class;
    .dynamically_generated_class {
        background-color: red;
    }
");

Scoped css

Scoped css is emitted using the scoped_css! macro, which takes a string literal. This uses Css Modules to generate unique class/id/variable names for each invocation at compile time. it then emits the transformed css to the build system. and expands to a set of constants mapping the initial name to the mangled one.

tip

Features such as the :global selector are supported as described in the css modules documentation.

extern crate natrix;
use natrix::prelude::*;
scoped_css!("
    .my-class {
        background-color: red;
    }
");

This will emit a css file with the following contents:

.SOME_HASH-my-class {
  background-color: red;
}

and will expand to the following in rust:

pub(crate) const MY_CLASS: &str = "SOME_HASH-my-class";

Which can then be used in a .class call.

tip

Use a module to make it more clear where the constants are coming from in the rest of your code.

extern crate natrix;
use natrix::prelude::*;
mod css {
    use natrix::prelude::scoped_css;
scoped_css!("
        .my-class {
            background-color: red;
        }
    ");
}

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

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.

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.

Returning multiple types

Very often a callback might branch, and each branch would want its own return value. In this case you can use the .into_box method to convert the return value into a Box<dyn Element>.

Alternatively you can use a Result or Either (behind the either feature) to return multiple types.

.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])

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))
                .into_box()
        } else {
            "None".into_box()
        }
    })
     }
}

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())
                .into_box()
        } else {
            "None".into_box()
        }
    })
     }
}

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::*;
#[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.option) {
            e::h1()
                .text(move |ctx: R<Self>| ctx.get(&guard))
                .into_box()
        } else {
            "None".into_box()
        }
    })
     }
}

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.

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::*;

#[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.

Message Passing

Due to the fact message passing between components uses async, you will need to make your test async as well to observe the changes. Luckily wasm-bindgen-test already natively supports async tests, so you can just use the async keyword in your test function. To wait until all messages have been processed, you can use next_animation_frame from the async_utils feature flag.

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

#[derive(Component)]
struct Child;

impl Component for Child {
    type EmitMessage = u8;
    fn render() -> impl Element<Self> {
        e::div().id("CHILD").on::<events::Click>(|ctx: E<Self>, _| {
            ctx.emit(1);
        })
    }
}

#[derive(Component)]
struct Parent {
    state: u8,
}

impl Component for Parent {
    fn render() -> impl Element<Self> {
        e::div()
            .child(e::div().id("PARENT").text(|ctx: R<Self>| *ctx.state))
            .child(C::new(Child).on(|ctx: E<Self>, msg| {
                  *ctx.state += msg;
            }))
    }
}

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

    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
async fn test_message_passing() {
    test_utils::mount_test(Parent {state: 0});

    let parent_element = test_utils::get("PARENT");
    let child_element = test_utils::get("CHILD");

    assert_eq!(parent_element.text_content(), Some("0".to_string()));
    child_element.click();
    assert_eq!(parent_element.text_content(), Some("0".to_string()));

    async_utils::next_animation_frame().await;
    assert_eq!(parent_element.text_content(), Some("1".to_string()));
}
}

fn main() {}

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.

Internals

Reactivity

Inter-Component Communication

Build System

Css Tree-shaking