Introduction

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:
- 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.
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.
- In release builds it will return
- 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.
- Using interior mutability to move a
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
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() {
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
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 Element
s 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!"))
;
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)
})
}
}
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])
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 contenttimestamp
: Creates a hash based on the current build timenone
: 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.