{{ }} you write is a live binding: the renderer evaluates it against current state, tracks which keys it depends on, and re-evaluates whenever any of them change. The display stays current automatically, so you write the expression once and the renderer handles the rest.
The expression language is intentionally bounded. Prefab UIs are designed to be safely serializable, so the expression grammar covers arithmetic, comparisons, boolean logic, conditionals, and formatting. That’s enough to drive any display while keeping business logic where it belongs: in your Python code or in a server action.
Template expressions
The{{ }} syntax is the protocol-level unit of reactivity. Any string prop on any component can contain {{ }} expressions, and the renderer treats each one as a live dependency.
Text("{{ count }}") displays whatever count holds and updates whenever it changes. The SetState actions write new values to count, and the text responds automatically. The expression {{ count + 1 }} computes before writing: the renderer evaluates it against current state each time. The reactivity is built in, so you write the binding once and the renderer keeps everything in sync.
Expressions can appear anywhere a string prop does. Button(disabled="{{ loading }}") disables a button based on state. Badge(label="{{ items | length }}") shows a count. Progress(value="{{ percent }}") drives a progress bar. The renderer re-evaluates each expression whenever any of its referenced keys change.
The Rx class
The expression language is accepted everywhere in the protocol, but writing{{ }} strings by hand in Python means giving up autocomplete, refactoring support, and typo detection. Rx lets you write the same expressions as Python code, with full editor support.
Rx("key") creates a reactive reference to a state key. The object it returns compiles to {{ key }} when serialized, but in your Python code it behaves like a regular value. You can do arithmetic on it, compare it, format it, and embed it in f-strings. Each operation returns a new Rx that represents the composed expression:
Rx returns a new Rx, building an expression tree. At serialization time, that tree renders to the correct {{ }} protocol syntax. You work in Python; the protocol gets the right strings.
Method calls on Rx compile to pipes in the protocol, which format and transform values for display. The Formatting with pipes section below covers them in detail, but here’s the basic idea:
Rx. Each Rx reference inside an f-string becomes a separate {{ }} interpolation in the output. Surrounding text stays as literal strings:
The .rx shorthand
Every interactive component manages a state key through itsname prop, and that state can be read by any other component or action that knows the key name. The challenge is that key names are often auto-generated (like slider_22) or defined far from where you need them. Passing string key names around is fragile and hard to refactor.
The .rx property solves this. It returns Rx(component.name), giving you a reactive reference to that component’s state without needing to know or repeat the key name:
Drag the slider and the text and progress bar update together. slider.rx compiles to {{ slider_22 }} (the auto-generated name), but you never need to know or type that key. The dependency is expressed through the Python variable, which keeps your code readable and refactor-safe.
Because .rx returns an Rx object, all the same operations work on it. slider.rx.number() formats the value. slider.rx > 50 produces a boolean expression. slider.rx.then("High", "Low") creates a conditional.
Use .rx when you have a component reference in scope. Use Rx("key") when working with keys from set_initial_state(), ForEach iteration, or state keys that belong to components you don’t have a reference to.
Sometimes you need a component’s .rx before the component exists — a label above a slider that shows its current value, for instance. Rx(lambda: slider) defers resolution until render time, when the variable is available. See Forward References for details.
Operators
Rx overloads Python’s operators so arithmetic, comparisons, and logic compile naturally to their protocol equivalents. Each operation returns a new Rx representing the composed expression.
Arithmetic
Standard math operators work onRx objects: +, -, *, /, and unary negation. The result is always a new Rx that compiles to the corresponding protocol expression.
price * quantity compiles to {{ price * quantity }}, and .currency() formats the result for display. The Expression Reference has the complete operator table.
String concatenation
F-strings are the cleanest way to mix reactive values with literal text. EachRx reference becomes a separate {{ }} interpolation:
+ operator also concatenates when either operand is a string: {{ 'Hello, ' + name }}. F-strings are almost always more readable.
Comparisons
Six comparison operators (==, !=, >, >=, <, <=) return boolean expressions. Comparisons are the foundation for conditional rendering with If/Elif/Else and conditional values with .then():
If component receives a boolean expression and only renders its children when the expression is true. When inventory changes, the conditions re-evaluate and the display updates.
Logical operators
Python reservesand, or, and not as keywords, so Rx uses the bitwise operators &, |, and ~ instead. They compile to &&, ||, and ! in the protocol.
The || operator short-circuits, which makes it useful as a falsy default: {{ name || 'Anonymous' }} returns 'Anonymous' when name is falsy (empty string, false, 0, or undefined). For null/undefined-only defaults, use the default pipe instead; see Expression Reference.
Ternary
.then(if_true, if_false) chooses between two values based on a boolean expression. It compiles to the ternary operator ? : in the protocol:
Toggle the switch and the text updates instantly. .then() works on any boolean Rx, including comparison results: (score >= 90).then("Pass", "Fail"). For choices beyond a simple two-way branch, If/Elif/Else is cleaner.
Formatting with pipes
Raw state values often need formatting before display. Pipes transform expression results: formatting numbers as currency, dates as readable strings, arrays into counts, and more. In the protocol, pipes use the| syntax: {{ revenue | currency }}. In Python, they’re method calls on Rx: revenue.currency().
Number formatting
.currency() and .number() add locale-aware grouping separators. .round() rounds without adding separators, which is useful for computations that feed into other expressions.
Date and time formatting
String formatting
.truncate() adds an ellipsis when the string exceeds the given length.
Array pipes
selectattr and rejectattr filter arrays by a boolean attribute, which is particularly useful with .length() for counts: todos.selectattr("done").length() gives you the number of completed items.
Chaining and defaults
Pipes chain left to right; each receives the output of the previous one:.default() pipe provides a fallback value when the key is null or undefined. Unlike ||, which triggers on any falsy value (including 0 and empty string), .default() only triggers on null/undefined. Use .default() when 0 or "" are valid values you want to preserve.
The Expression Reference has the complete pipe catalog with detailed examples for every formatter.
Type preservation
When anRx expression is the entire value of a prop, the renderer resolves it to the appropriate JavaScript type. Progress(value=slider.rx) serializes to "{{ slider_22 }}", and the renderer resolves it to the number 50, preserving the type for props that expect numbers or booleans.
When an expression is embedded in surrounding text, the result is always a string. f"Volume: {slider.rx}%" resolves to "Volume: 50%" because the surrounding text forces string concatenation.
The practical rule: if a prop expects a number or boolean (value, min, max, disabled, checked), pass the Rx expression as the sole value. Wrapping it in an f-string converts the result to a string, which may cause unexpected behavior.