understanding sycamore reactivity

2022-03-29 ยท 2 min read

Docs: https://sycamore-rs.netlify.app/docs/basics/reactivity

Whenever reactivity is used, there must be a reactive scope.

What is a reactive scope? Ex: Scope provides a reactive scope.

A Signal wraps a value and lets you listen for changes to that value.

To actually listen on a Signal, you need to call signal.get() inside a listener scope (which is not the same as a reactive scope).

Notably, #[component] function bodies do not provide a listener scope, so calling signal.get() in a component won't re-render the component if the signal changes. However, the view! { ctx, _ } macro body provides an implicit listener scope when interpolating.

methodargsreturnsdescription
ctx.create_effectFnMut()()calls closure when a signal changes (signal.get() must be called in the closure to register it). used to impl most other methods here
ctx.create_memoFnMut() -> U&ReadSignal<U>calls closure when a registered signal changes and wraps the return value as a derived signal.
ctx.create_selectorFnMut() -> U&ReadSignal<U>calls closure when a registered signal changes. like create_memo but the derived signal only notifies if the return value is actually different from the previous value (Note: U must impl PartialEq).
ctx.create_reducerU, FnMut(&U, Msg) -> U(&ReadSignal<U>, Fn(Msg))calls closure whenver the message handler Fn(Msg) gets called. kind of like an async fold, where the closure is an actor message handler.
signal.mapctx, FnMut(&T) -> U&ReadSignal<U>calls closure with signal value whenever signal changes. returns derived signal with return value.

For (all?) of the above methods, we always call the closure once initially. This also allows us to observe all the signal.get()s inside the closure, which we need to register which signals to listen on.

For example, to see how reactivity works with the view! macro, we can look at how it (approximately) is expanded:

// before expansion
let state = ctx.create_signal(0);
view! { ctx,
	p {
		(state.get())
	}
}

// after expansion
let state = ctx.create_signal(0);
{
    let element = GenericNode::element(p);
    let text = GenericNode::text(String::new());
	// Update text when `state` changes.
    ctx.create_effect(move || {
        text.update_text(Some(&state.get()));
	});
    element.append(&text);
    element
}

As you can see, interpolations happen inside an implicit create_effect, which does not happen for signals retrieved outside a view! macro.