Reactivity

Callbacks

You have already seen |ctx: R<Self>| ... used in the varying examples in the book. Lets go into some more detail about what this does.

The callbacks return a value that implements Element, internally the framework will register which fields you accessed. And when those fields change, the framework will recall the callback and update the element with the result.

Natrix does not use a virtual dom, meaning when a callback is re-called the framework will swap out the entire associated element. See below for tools to mitigate this.

important

Natrix assumes render callbacks are pure, meaning they do not have side effects. If you use stuff like interior mutability it will break framework assumptions, and will likely lead to panics or desynced state. For example using interior mutability to hold onto a Guard outside its intended scope will invalidate its guarantees.

Returning multiple types

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

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

.watch

Now imagine you only access part of a field.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    counter: u8,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        format!("{}", *ctx.counter > 10)
    })
     }
}

This will work, but it will cause the callback to be called every time counter changes, even if it causes no change to the dom. In this case thats fine, its not a expensive update, but imagine if this was a expensive operation. This is where .watch comes in.

What it does is cache the result of a callback, and then it calls it on any change, but will compare the new value to the old value. And only re-runs the surrounding callback if the value has changed.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    counter: u8,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        let bigger_than_10 = ctx.watch(|ctx| *ctx.counter > 10);
        format!("{}", bigger_than_10)
    })
     }
}
Actionctx.watch(...) runsformat!(...) runs
initial renderyesyes
0 -> 1yesno
10 -> 11yesyes
11 -> 12yesno
11 -> 10yesyes

This can be more usefully used for example when dealing with a Vec of items. For example ctx.watch(|ctx| ctx.items[2])

guard_...

Problem

Now you could imagine .watch being useful for Option and Result types. It is, but you have to be careful about how you use it.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    option: Option<u8>,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        if ctx.watch(|ctx| ctx.option.is_some()) {
            let value = ctx.option.unwrap();
            e::h1()
                .text(format!("Value: {}", value))
                .into_box()
        } else {
            "None".into_box()
        }
    })
     }
}

This will work, but it will still cause the callback to be called every time option changes because it still uses ctx.option directly. As with any fine-grained reactivity, you can use nested callbacks to get a better effect.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    option: Option<u8>,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        if ctx.watch(|ctx| ctx.option.is_some()) {
            e::h1()
                .text(|ctx: R<Self>| ctx.option.unwrap())
                .into_box()
        } else {
            "None".into_box()
        }
    })
     }
}

And this does work exactly as we want it to. But there is one downside. That .unwrap, we know for a fact that it will never panic because of the condition above. But because in rusts eyes these callbacks are not isolated we cant prove to it otherwise. In addition that unwrap means you cant use the recommended lint to deny them.

Solution

This is where the guard_option (and guard_result) macros come in. They use ctx.watch internally, and gives you a way to access the value without having to unwrap it.

extern crate natrix;
use natrix::prelude::*;
#[derive(Component)]
struct HelloWorld {
    option: Option<u8>,
}
impl Component for HelloWorld {
    fn render() -> impl Element<Self> {
e::div()
    .child(|ctx: R<Self>| {
        if let Some(guard) = guard_option!(ctx.option) {
            e::h1()
                .text(move |ctx: R<Self>| ctx.get(&guard))
                .into_box()
        } else {
            "None".into_box()
        }
    })
     }
}

warning

Similarly to DeferredRef you should not hold this across a yield point. guard_option does in fact still use .unwrap() internally, meaning its effectively the same as the "bad" code above. It is simply a nice api that enforces the invariant that you only .unwrap in a context where you have done the .is_some() check in a parent hook.