Runtime expressions
The expr! macro defines an expression that can run on the server and in the browser. It compiles to Rust for the initial render and JavaScript for browser updates. Inside view!, write runtime expressions as $(...).
Expressions support a subset of Rust with matching behavior in both languages. The supported types, operations, and syntax are described below.
let show = signal(cx, || false);
Ok(view! {
$(if show.get() { "shown" } else { "hidden" })
})
The server evaluates the expression for the initial HTML. In the browser, it runs again whenever a signal it reads changes and updates the DOM without a server request.
The macro returns an Expr containing the server result and browser code.
Server evaluation is synchronous. An expression that reads no signals renders as static content. An event-handler closure's body runs only when the browser invokes it.
Captured variables
An identifier that is not defined inside the expression is captured from the surrounding Rust scope:
let count = signal(cx, || 0usize);
Ok(view! {
$(count.get() + step)
})
Capturing clones the value, so surrounding code can still use it. The browser receives a snapshot from the render. Later server changes do not update that snapshot. Captured values must use the supported types described below. Capturing an owned collection clones its elements. Capturing a slice borrows its elements on the server, but still sends a snapshot to the browser.
A captured Expr<T> behaves as T inside another expression. The server reuses its evaluated value. The browser evaluates its expression at each use, so signal reads remain reactive, including inside event handlers.
let selected = signal(cx, || "overview".to_owned());
let active = expr!(selected.get() == "overview");
let label = expr!(if active { "Selected" } else { "Select" });
The shared vocabulary
Runtime types expose a subset of their Rust APIs:
f64: arithmetic (+,-,*,/), comparisons, and negation. Floating-point literals aref64. Text output follows Rust'sDisplay, including decimal notation and the spellingsinf,-inf, and-0.- Rust integer types: arithmetic (
+,-,*,/,%), comparisons, and negation for signed types. Unsuffixed integer literals areusize; use a suffix for another type, such as42u64or-1i32. Operands must have the same type. Values retain their full precision in the browser, including 128-bit integers, and pointer-sized integers use the server target's width. Arithmetic panics on overflow, division by zero, or remainder by zero in both debug and release builds. SignedMIN / -1,MIN % -1, and negatingMINalso panic. bool:!, equality comparisons,then, andthen_some.Stringand&str:len,is_empty,trim,trim_start,trim_end,starts_with,ends_with,contains,to_owned, and comparisons. The result oflenhas typeusize.Option<T>:is_some,is_none,unwrap, andexpect.Result<T, E>:is_ok,is_err,ok,err,unwrap,expect,unwrap_err, andexpect_err.Vec<T>,[T; N], and slices:len,is_empty,get,index,first,last,to_vec, andto_owned. Vectors and arrays also supportas_sliceandclone. Lengths and indexes areusize.getreturnsNonefor an out-of-bounds index;indexpanics. Both borrow the element. Elements must belong to the shared vocabulary.- Tuples of vocabulary types: literals such as
(count.get(), "items"), field access such aspair.0, andclone. Empty tuples()and single-element tuples(value,)are supported. A field of a borrowed tuple, such asvalues.index(0).0, borrows the element. A tuple renders its elements one after another with no separator. Tuples do not support comparisons. - Structs with
#[record]: create values with struct literals and access fields by name, such asorder.id. Access through a borrowed record borrows the field. Records that implementClonealso supportclone. Expressions cannot compare records or render them directly. Signal:getandset, plus a shorter spelling for common writes:toggleon aboolsignal,incrementanddecrementon a numeric signal, andpush_stron aStringsignal.
Operations follow Rust semantics in both languages. For strings, len counts UTF-8 bytes, comparisons use code point order, and trimming uses Unicode White_Space. This means trimming keeps U+FEFF and removes U+0085.
Supported syntax
Expressions use a subset of Rust's syntax:
- String, integer,
f64, andboolliterals. - The unary and binary operators listed above.
- Method calls, field access, and indexing.
- Construction of options with
SomeandNone, results withOkandErr, tuples with tuple literals, and records with struct literals. - Blocks, with
letbindings of plain identifiers; the trailing expression is the block's value. if/elseas an expression.- Closures, optionally
async, and.await. loop,while,break,continue, andreturn.
Unsupported syntax produces a compile error at the expression.
Embedding JavaScript
Use raw! for JavaScript operations outside the supported vocabulary. Its first argument is JavaScript source. The optional second argument is equivalent Rust for server evaluation:
let name = signal(cx, String::new);
Ok(view! {
$({
let n = name.get();
raw!("${n}.toUpperCase()", n.to_uppercase())
})
})
${ident} in the JavaScript string inserts a binding from the expression's scope. Omitting the Rust argument restricts the expression to browser execution. You are responsible for keeping the Rust and JavaScript behavior equivalent.
Signal reads in the Rust fallback determine whether the expression needs a browser binding. The fallback must therefore read the signals its JavaScript depends on, even if their initial values happen to produce a constant result.