Expand description
§Terrazzo
Terrazzo is a lightweight, simple and efficient web UI framework based on Rust and WASM.
Tl;DR: See Terrazzo in action in the demo.
§Prior art
This project was inspired by frameworks like Dioxus and Leptos.
These frameworks are based on Rust and WASM:
- Both the server-side and client-side logic is written in Rust
- Rust↔Javascript interop based on wasm_bindgen allows creating dynamic web pages using the DOM API.
Like many other frameworks, the core is built around a simple concept: reactivity. When a function computes a component (i.e: an HTML node), it records which signals are being used. Then, whenever one of those signals changes, the function is automatically re-evaluated again, and the UI is updated.
The implicit reactive ownership tree derives from the UI components tree (the DOM), and allows making
the signals arena-allocated. In Rust terms, it means Signal
can be Copy
and not just Clone
,
which greatly improves the ergonomics.
- We don’t have to guess how and when to call
.clone()
, especially when signals are used in closures. - We can always pass signals as values so we don’t have to deal with references, lifetimes and the Rust borrow-checker.
In other words, we can leverage the powerful Rust type system, use one language for both the UI and the backend server implementation, and get all the benefits of the rich Rust ecosystem.
§Why Terrazzo?
The goal of Terrazzo isn’t to replace Dioxus or Leptos. It’s a lightweight, bare-bones alternative that aims to achieve one simple task and do it well: a templating system for UI.
Dioxus and Leptos are incredibly feature-rich, but are also prone to bugs.
§Arena-allocated signals and use-after-free bugs
I believe that making signals Copy
using arena allocation for the sake of ergonomics is an
anti-pattern.
- With Dioxus, use of signals must obey a strict set of rules can that cannot be enforced otherwise by the Rust compiler. https://dioxuslabs.com/learn/0.6/reference/hooks/#rules-of-hooks
- With Leptos, bugs can arise if signals are used after they are (implicitly) disposed. I feel like this is completely missing the point of using Rust, since once of the main selling points of this language is precisely to prevent use-after-free bugs. Appendix: The Life Cycle of a Signal
I prefer dealing with the Rust borrow-checker and any other kind of static analysis annoyance, even
if it means I have to add explicit calls to .clone()
and add some extra boilerplate. This is a
small price to pay if I can avoid wasting time debugging large classes of bugs.
The promise of Rust is that the compiler has your back: if it compiles, it works. Rust code runs
faster than other languages, not because “for-loops” are faster in Rust, but because Rust codebases
are easier to refactor and optimize. You can replace a deep copy with a reference, and that promise
will hold: if it compiles, it works. Else, the Rust compiler will help you figure out when to call
.clone()
, when to use use ref-counting pointers, or when to guard mutable state with a mutex or
use a cell.
§Hydration bugs
Server-side rendering is a hard-to-use feature. It only works if the server-side code generates the same page as the client-side code would. In theory, they should always match since the exact same code runs server- and client-side, it’s just an optimization. In practice, it’s not necessarily the case, so avoiding these bugs requires careful debugging and testing. Hydration Bugs (and how to avoid them)
§Custom tooling
One of the biggest selling points for Rust is strong tooling, including cargo
, rustfmt
and
clippy
.
- The Dioxus CLI is an unnecessary annoyance
- The
rsx! { ... }
andview! { ... }
macros to write HTML templates look nice at first, but don’t work with the standard Rust formatter.
§What does Terrazzo look like?
Terrazzo does not need custom tooling. The autoclone!()
macro helps with cloning. SSR with
hydration is not supported yet but Terrazzo useds a simple diff-merge logic that isn’t prone
to bugs.
Doing the right thing should be easy, doing the wrong thing should be hard:
Terrazzo makes it easier to optimize rendering: reading a signal requires declaring a template, so
just make sure to push reading signals down to child DOM nodes. Only read a signal where you need
to render something. Use the key
special attribute to avoid re-creating DOM nodes when ordering
changes but the nodes stay the same.
Terrazzo uses two different macros:
- The
#[template]
turns a function into a template. Use#[template(debug = true)]
to see what the generated code looks like. - The
#[html]
adds syntactic sugar to replace function calls where the name matches one of the well-known HTML tags into a Rust struct representing an HTML tag. Use#[html(debug = true)]
to see what the generated code looks like.
#[template]
#[html]
pub fn my_main_component() -> XElement {
let state = State {
value: 123,
signal: XSignal::new("signal", "state".to_owned()),
};
let state_value = state.value;
return div(
class = "main-component",
style::width = "100%",
click = move |event| state.click(),
"text node {state_value}",
static_component(),
dynamic_component(state.signal.clone()),
);
}
#[template(tag = div)]
#[html]
fn static_component() -> XElement {
tag("static value")
}
#[template(tag = div)]
#[html]
fn dynamic_component(#[signal] value: String) -> XElement {
tag("Dynamic: ", "{value}")
}
See demo.rs.
Re-exports§
Modules§
- owned_
closure - Owned Javscript closures
- prelude
- Public-facing client library
- static_
assets - Server-side assets
- widgets
- Reusable UI components.
Macros§
- __
diagnostics_ debug - __
diagnostics_ debug_ span - __
diagnostics_ enabled - __
diagnostics_ error - __
diagnostics_ error_ span - __
diagnostics_ info - __
diagnostics_ info_ span - __
diagnostics_ trace - __
diagnostics_ trace_ span - __
diagnostics_ warn - __
diagnostics_ warn_ span - declare_
asset - Declares a file as a static asset.
- declare_
assets_ dir - Macro to load a folder of static assets.
- declare_
scss_ asset - Declares a scss file as a static asset.
- declare_
trait_ aliias
Functions§
- install_
assets - Installs assets from the shared library
- setup_
logging