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(())
});
})
}
}