Adding State
Right now, our app is completely static. We will now see how we can add interactivity to our app using reactive state.
Reactivity
Sycamore’s fine-grained reactivity is based on SolidJS. If you’ve used SolidJS before, a lot of this will be very similar.
Signals
Reactive state is built from reactive nodes. The simplest example of a reactive
node is a signal. This can be thought of as a simple wrapper around a type
that can keep track of when it is accessed and when it is updated. To create a
new signal, use the create_signal
function.
let signal = create_signal(123);
By convention, functions related to reactive state should be called either
create_*
oruse_*
and are called “hooks”. We will see many other examples of such hooks soon.
The value inside a signal can be read or written. By default, the Signal::get
method will copy the value inside the signal so that the wrapped type must
implement Copy
. If you have a non copyable type, you can use
Signal::get_clone
instead.
let signal = create_signal(123);
// Should print `123`.
console_log!("{}", signal.get());
// Update the signal with a new value.
value.set(456);
// Should print `456`.
console_log!("{}", signal.get());
// `Signal<T>` also implements `Display` so this is the same as the above.
console_log!("{signal}");
We are using
console_log!
here instead ofprintln!
because on thewasm32-unknown-unknown
target, the Rust standard library does not have access to web APIs and soprintln!
does not do anything. Theconsole_log!
macro instead calls theconsole.log
function which prints the value into the JavaScript console.
Signals are also examples of a cell-type in Rust that provides
interior-mutability. What this means concretely is that Signal::set
just takes
a normal (immutable) reference instead of a mutable reference. This allows us to
bypass Rust’s XOR mutability restriction so that we can update the signal from
multiple places in our code.
Effects
Suppose we want to print the signal every time the value changes. A simple way
to do this would be to add a println!
every time we call Signal::set
.
However, this quickly becomes tedious and error-prone. Instead, we can use
effects. Effects are functions that get called every time one of its
dependencies is updated. Dependencies are automatically tracked whenever they
are used from inside the function.
let signal = create_signal(123);
create_effect(move || {
// Using `.get(...)` automatically tracks this signal as a dependency.
let value = signal.get();
console_log!("{value}");
// Or we can use the shorter: `console_log!("{signal}");`
});
signal.set(456);
signal.set(789);
This prints the following to the terminal:
123
456
789
Notice how we used the move
keyword to move our signal into the effect closure
but we can still use it from outside the effect. This is because Signal<T>
implements Copy
, even when T
is not Copy
. Internally, signals are
allocated on an arena-like structure which stores the value and the Signal<T>
type is simply an id into this arena, which allows it be Copy
. The benefit of
this should be obvious: if signals were not copyable, we would need to clone
them every time we used them from inside a closure.
Memos and derived state
We can also create derived state, that is, state that is a function of other
state. The simplest way to do this is to create a closure. Now, every time we
access our derived signal, we will also automatically track signal
. So we can
write something like:
let signal = create_signal(1);
let derived = move || signal.get() * 2;
create_effect(move || {
let value = signal.get();
let doubled = derived();
console_log!("signal = {signal}, doubled = {doubled}");
})
Effects automatically de-duplicate dependencies so here, signal
is only
tracked a single time even though it is accessed twice, once in signal.get()
,
and another time when calling derived()
.
However, if instead of simply doubling our value, what if we performed a more
expensive computation. In this case, if we accessed derived
multiple times, we
would be wasting a lot of work by recalculating our derived state although
signal
has not changed. We can prevent this by using memos, which store
the value of the derived signal and only updates the value when its dependencies
changes.
let signal = create_signal(1);
let derived = create_memo(move || expensive_computation(signal.get()));
// The memo will only call the closure once so long as `signal` hasn't changed.
let foo = derived.get();
let bar = derived.get();
// This also triggers `derived` to recompute its value.
signal.set(2);
The create_memo
hook returns a ReadSignal<T>
which is a signal that can only
be read, not written to. The Signal
type automatically dereferences to a
ReadSignal
so you can use a Signal
wherever a ReadSignal
is expected.
Effects are actually implemented using memos. Semantically speaking, this is not really correct since we use effects for creating side-effects and memos for pure computations. However, implementation wise, an effect is simply a memo that does not return a value.
Reactive views
Now that we have all this reactive machinery in place, let’s see how we can use it to manage our app’s state. What we want to do is to store our state in reactive nodes and then keep the UI automatically in sync with the state.
Sycamore makes this extremely easy. Simply access the reactive state from inside
an interpolated fragment in the view!
macro and the UI will automatically
update to any state changes.
let counter = create_signal(1);
view! {
p { "Count: " (counter) }
}
What this does, behind the hood, is essentially something like this:
let counter = create_signal(1);
let text1 = document().create_text_node("Count: ");
let text2 = document().create_text_node(counter.get().to_string());
create_effect(move || {
// This effect is automatically run whenever `counter` is changed.
text2.set_text_content(counter.get().to_string());
});
We can of course use derived state in views as well:
let counter = create_signal(1);
let doubled = create_memo(move || counter.get() * 2);
view! {
p { "Count: " (counter) }
p { "Doubled: " (doubled) }
}
Event handlers
Of course, displaying the state in our UI is not all that useful if we can’t update the state. There are many places where you might update the state but the most common place is probably inside an event handler.
Sycamore makes it easy to add event handlers to your view, using the on:*
directive. For example, the following code adds an event handler to the click
event and increments the counter by 1.
view! {
button(on:click=move |_| counter.set(counter.get() + 1)) { "Increment" }
p { "Count: " (counter) }
}
Many times, we want to keep the view code on the simpler side so we can extract the event handler into a local variable:
let increment = move |_| counter.set(counter.get() + 1);
view! {
button(on:click=increment) { "Increment" }
}
A nice trick is that Signal
actually implements std::ops::AddAssign
which
lets us write:
// Notice the `mut` here. This is required because of the `AddAssign` trait.
let mut counter = create_signal(1);
let increment = move |_| counter += 1;
Try this out! The complete code for this example is:
use sycamore::prelude::*;
#[component]
fn App() -> View {
let mut counter = create_signal(1);
let doubled = create_memo(move || counter.get() * 2);
let increment = move |_| counter += 1;
view! {
button(on:click=increment) { "Increment" }
p { "Count:" (counter) }
p { "Doubled: " (doubled) }
}
}
fn main() {
sycamore::render(App);
}
Now every time we click on the button, the text displaying the counter will automatically update.
Conditional views
Instead of just displaying the state as text on screen, we can also display part of the UI depending on whether some reactive condition is fulfilled.
// Create a new bool signal representing whether we show this part of the UI or not.
let show = create_signal(true);
view! {
(if show.get() {
view! {
p { "Now you see me" }
}
} else {
view! {
p { "Now you don't" }
}
})
}
Reactive components
As we saw before, components in Sycamore are just regular Rust functions. How
can we pass reactive state to components? Simple! Just wrap the prop inside a
ReadSignal
(or a Signal
if you need to mutate the value inside the
component). For example, we can write:
#[component(inline_props)]
fn CounterDisplay(value: ReadSignal<i32>) -> View {
view! {
div {
p { "Counter value: " (value) }
}
}
}
let counter = create_signal(1);
view! {
CounterDisplay(value=counter)
}
However, this is not as flexible as it could be. We can pass signals and memos,
but we cannot pass, for instance, a simple derived closure. To fix this
limitation, you can use the MaybeDyn<T>
prop instead.
// `#[prop(setter(into))]` let's us use anything that implements `Into<MaybeDyn<T>>`.
// This includes both `Signal` and `ReadSignal` as well as functions.
#[component(inline_props)]
fn CounterDisplay(#[prop(setter(into))] value: MaybeDyn<i32>) -> View { ... }