Leptos 16: Interlude – Reactivity and Functions

Leptos 16: Interlude - Reactivity and Functions

Interlude: Reactivity and Functions

One of our core contributors recently told me, “I didn’t use closures so frequently until I started using Leptos.” This is indeed true. Closures are at the heart of any Leptos application. Sometimes it seems a bit silly:

// A signal holds a value and can be updatedlet (count, set_count) = signal(0); // A derived signal is a function that accesses other signalslet double_count = move || count.get() * 2;let count_is_odd = move || count.get() & 1 == 1;let text = move || if count_is_odd() {    "odd"} else {    "even"}; // An effect automatically tracks the signals it depends on// and re-runs when they changeEffect::new(move |_| {    logging::log!("text = {}", text());});view! {    <p>{move || text().to_uppercase()}</p>}

Closures, everywhere closures!

But why?

Functions and UI Frameworks

Functions are at the core of every UI framework. This makes perfect sense. Creating a user interface essentially involves two phases:

  • Initial rendering
  • Updating

In web frameworks, the framework performs some initial rendering. Then it hands control back to the browser. When certain events are triggered (like mouse clicks) or asynchronous tasks are completed (like HTTP requests finishing), the browser wakes the framework to update something. The framework runs some code to update your user interface and then goes back to sleep until the browser wakes it again.

The key phrase here is “running some code.” The natural way to “run some code” at any given point in time—in Rust or any other programming language—is to call a function. In fact, every UI framework is based on repeatedly running some function:

  • Virtual DOM frameworks, like React, Yew, or Dioxus, repeatedly run components or render functions to generate a virtual DOM tree that can be coordinated (diff/patch) with previous results to update the DOM.
  • Compiled frameworks, like Angular and Svelte, break your component templates into “create” and “update” functions, re-running the update function when they detect a change in component state.
  • In fine-grained reactive frameworks, like SolidJS, Sycamore, or Leptos, you define those functions that are re-run.

This is what all our components are doing.

For example, in our simplest typical <span><SimpleCounter/></span> example:

#[component]pub fn SimpleCounter() -> impl IntoView {    let (value, set_value) = signal(0);    let increment = move |_| *set_value.write() += 1;    view! {        &lt;button on:click=increment&gt;            {value}        &lt;/button&gt;    }}

<span>SimpleCounter</span> function itself runs only once. The <span>value</span> signal is created only once. The framework hands the <span>increment</span> function to the browser as an event listener. When you click the button, the browser calls the <span>increment</span> function, which updates the <span>value</span> through <span>set_value</span>. This updates the single text node represented by <span>{value}</span> in our view.

Functions are the key to reactivity. They provide the framework with the ability to re-run the application at the smallest possible unit in response to changes.

So remember two things:

  • Your component function is a setup function, not a render function: it runs only once.
  • To make values in the view template reactive, they must be reactive functions: either signals or closures that capture and read signals.

This is actually the main difference between the stable and nightly versions of Leptos. As you know, using the nightly compiler and <span>nightly</span> features allows you to call signals directly as functions: so it’s <span>value()</span> instead of <span>value.get()</span><span>.</span>

But this is not just syntactic sugar. It allows for an extremely consistent semantic model: reactive things are functions. Access signals by calling functions. To say “give me a signal as a parameter,” you can accept anything that implements <span>impl Fn() -> T</span>. This function-based interface does not distinguish between signals, memos, and derived signals: any of them can be accessed by calling them as functions.

Unfortunately, implementing <span>Fn</span> traits on arbitrary structs like signals requires nightly Rust, although this particular feature has mostly just been shelved and is unlikely to change (or stabilize) anytime soon. For some reason, many people avoid using nightly. Therefore, over time, we have shifted the default settings for documentation and other content to the stable version. Unfortunately, this has made the simple mental model of “signals are functions” less intuitive.

Leave a Comment