Introduction

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:
- Rust/Cargo
- wasm-bindgen
- wasm-opt, Usually installed via
binaryen
for your platform.
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 usesset_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.
- In release builds it will return
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
Component
s 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
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 Element
s 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!"))
;
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)
})
}
}
Action | ctx.watch(...) runs | format!(...) runs |
---|---|---|
initial render | yes | yes |
0 -> 1 | yes | no |
10 -> 11 | yes | yes |
11 -> 12 | yes | no |
11 -> 10 | yes | yes |
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.