Reactivity
Reactivity is at the heart of Sycamore.
Instead of relying on a Virtual DOM (VDOM), Sycamore uses fine-grained reactivity to keep the DOM and state in sync. In fact, Sycamore’s reactivity system can be used on its own without pulling in all the DOM rendering part. It just turns out that fine-grained reactivity and UI rendering are a great match which is the whole point of Sycamore.
Reactive scopes
Whenever reactivity is used, there must be a reactive scope. Such a scope is provided by functions
such as sycamore::render
and sycamore::render_to
as an argument to the render closure.
;
render
From this point on, we assume all code, unless otherwise specified, is run inside such a scope so
that it can access cx
.
Signal
Reactivity is based on reactive primitives. A Signal
is one such example of a reactive primitive.
At it’s simplest, a Signal
is simply a wrapper around a type that can be read and written to and
which can be listened on whenever its wrapped value is mutated.
To create a signal, we use create_signal(cx, ...)
. Note that the return value of this method is
not actually Signal
but &Signal
. The reason for this is because the created signal is allocated
on the reactive scope and therefore has its lifetime tied with the scope. Furthermore, this allows
using Rust’s lifetime system to make sure signals are not accessed once its enclosing scope has been
destroyed.
Here is an example of creating a signal, accessing it via .get()
, and modifying it via
.set(...)
.
let state = create_signal; // Create a reactive atom with an initial value of `0`.
println!; // prints "The state is: 0"
state.set;
println!; // should now print "The state is: 1"
Effects
We mentioned earlier that signals can be listened on to tell us whenever its value has changed. Let’s do that! For example, imagine we wanted to print out every state change. This can easily be accomplished like so:
let state = create_signal;
create_effect;
// Prints "The state changed. New value: 0"
// (note that the effect is always executed at least 1 regardless of state changes)
state.set; // Prints "The state changed. New value: 1"
state.set; // Prints "The state changed. New value: 2"
state.set; // Prints "The state changed. New value: 3"
How does the create_effect(...)
function know to execute the closure every time the state changes?
Calling create_effect
creates a new “listener scope” (not to be confused with reactive scope)
and calling state.get()
inside this listener scope adds itself as a dependency. Now, when
state.set(...)
is called, it automatically calls all its dependents. In this case, whenever
state
is updated, the new value will be printed!
Memos
Sure, effects are nice but Rust is a multi-paradigm language, not just an imperative language. Let’s take advantage of the more functional side of Rust!
In fact, we can easily create a derived state (also know as derive stores) using create_memo(...)
.
let state = create_signal;
let double = create_memo;
assert_eq!;
state.set;
assert_eq!;
create_memo(...)
automatically recomputes the derived value when any of its dependencies change.
Now that you understand the basics of Sycamore’s reactivity system, we can take a look at how this is used together with UI rendering.
Using reactivity with DOM updates
Reactivity is automatically built-in into the view!
macro. Say we have the following code:
let state = create_signal;
view!
This will expand to something approximately like:
let state = create_signal;
If we call state.set(...)
somewhere else in our code, the text content will automatically be
updated!
Common pitfalls
Dependency tracking is topological, which means that reactive dependencies (like a Signal
) must
be accessed (and thus recorded as reactive dependencies) before the listener scope (like the one
in a create_effect
) returns.
For example, code inside the spawn_local
won’t be tracked:
create_effect
We’ll find that any Signal
s we track in the create_effect
won’t be tracked properly in the
wasm_bindgen_futures::spawn_local
, which is often not what’s intended. This problem can be gotten
around by accessing reactive dependencies as needed before going into a future, or with this simple
fix:
create_effect
All we’re doing there is accessing the dependency before we move into the future, which means dependency tracking should work as intended.