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