Component Binding and Props in euv
Project Code:https://github.com/euv-dev/euv
euv is a Rust + WASM frontend UI framework that leverages a virtual DOM and reactive signals. One of its core strengths is its component system, which enables developers to build reusable, composable UI elements with strong type safety and flexible data flow patterns. In this article, we will explore the component binding mechanisms in euv in depth, covering the Props Down / Callback Up pattern, strongly typed props, custom callbacks, bidirectional binding via shared signals, and the watch! macro for cross-component reactive binding.
The Component Model
In euv, every component is a Rust function annotated with the #[component] macro. A component receives a VirtualNode that carries both props and child nodes, and returns a new VirtualNode representing its rendered output. Here is a basic example:
#[derive(Clone, Default)]
struct MyCardProps {
title: &'static str,
}
#[component]
pub fn my_card(node: VirtualNode<MyCardProps>) -> VirtualNode {
let MyCardProps { title, .. } = node.try_get_props().unwrap_or_default();
let children: VirtualNode = node.try_get_child_node();
html! {
div {
h3 { title }
children
}
}
}
This example demonstrates several key concepts:
-
Props struct:
MyCardPropsis a Rust struct withCloneandDefaultderives. It defines the shape of the data the component expects from its parent. -
#[component]annotation: This macro marks the function as an euv component, enabling it to participate in the virtual DOM rendering pipeline. -
try_get_props(): Extracts the typed props from the virtual node. Theunwrap_or_default()call ensures that if no props are provided, the struct's default values are used. -
try_get_child_node(): Retrieves any child content passed to the component, which can be forwarded into the rendered output.
Props Down / Callback Up
The primary data flow pattern in euv follows the "Props Down, Callback Up" principle. Parent components pass data to children via props, and children communicate back to parents via callback functions passed as props.
Passing Props Down
When a parent renders a child component, it provides props as a struct instance. The child component receives these props through its VirtualNode parameter:
#[derive(Clone, Default)]
struct MyCardProps {
title: &'static str,
}
#[component]
pub fn my_card(node: VirtualNode<MyCardProps>) -> VirtualNode {
let MyCardProps { title, .. } = node.try_get_props().unwrap_or_default();
let children: VirtualNode = node.try_get_child_node();
html! {
div {
h3 { title }
children
}
}
}
The title prop flows from the parent down to the my_card component. This unidirectional flow makes data dependencies explicit and easy to reason about.
Sending Callbacks Up
When a child needs to communicate with its parent — for example, when a user interacts with a form element — the parent passes a callback function as a prop. The child invokes this callback to notify the parent of the event. This pattern keeps the child component pure and reusable, while allowing the parent to own the state.
Strongly Typed Props
Unlike many JavaScript-based UI frameworks where props are loosely typed dictionaries, euv leverages Rust's type system to enforce prop types at compile time. Every prop struct is a concrete Rust type, and the compiler will reject any attempt to pass incorrect data:
#[derive(Clone, Default)]
struct MyCardProps {
title: &'static str,
}
Because title is typed as &'static str, the compiler guarantees that only string slices can be passed as the title prop. This eliminates an entire class of runtime errors that are common in dynamically typed frameworks.
The try_get_props() method on VirtualNode performs the type extraction. Combined with unwrap_or_default(), it provides a safe and ergonomic way to handle optional props:
let MyCardProps { title, .. } = node.try_get_props().unwrap_or_default();
The .. syntax ignores any fields not explicitly destructured, making it easy to work with structs that may grow over time.
Custom Callbacks
Components can accept callback functions as props, enabling flexible parent-child communication. A callback prop is simply a function pointer or closure stored in the props struct. When the child component detects a user interaction or state change, it calls the callback, passing any relevant data as arguments.
This pattern is particularly powerful when combined with euv's reactive signal system. A parent can pass a signal setter as part of a callback, allowing the child to directly update the parent's state in a controlled manner.
Bidirectional Binding with Shared Signals
While the Props Down / Callback Up pattern works well for most cases, there are scenarios where true bidirectional binding is more convenient. euv supports this through shared signals. A parent component creates a signal using use_signal and passes the signal itself — not just its value — to a child component. The child can then read from and write to the signal directly:
// Create a shared signal in the parent
let shared_text: Signal<String> = use_signal(|| "Type here...".to_string());
// The child component receives the Signal directly as a parameter
pub fn child_input(text_signal: Signal<String>, count_signal: Signal<i32>) -> VirtualNode {
html! {
div {
input {
r#type: "text"
value: text_signal.get()
oninput: on_input_value(text_signal)
}
}
}
}
In this example:
-
use_signal(|| "Type here...".to_string())creates a reactive signal initialized with the string"Type here...". - The
child_inputfunction receivestext_signal: Signal<String>directly — not a plain string value. -
text_signal.get()reads the current value of the signal for display in the input element. -
on_input_value(text_signal)is an event handler that writes the input's new value back to the signal whenever the user types.
Because both the parent and child hold references to the same signal, any update made by the child is immediately visible to the parent, and vice versa. This creates a seamless bidirectional binding without the need for explicit callback wiring.
Cross-Component Reactive Binding with watch!
The watch! macro is one of euv's most powerful features for cross-component reactive programming. It allows you to observe one or more signals and automatically execute a callback whenever any of them changes. This is ideal for scenarios where you need to derive state from multiple sources or synchronize state across components.
Watching a Single Signal
Suppose you have a temperature converter with Celsius and Fahrenheit values. When the user changes the Celsius input, the Fahrenheit value should update automatically:
watch!(celsius, |celsius_value: f64| {
fahrenheit.set(celsius_value * 9.0 / 5.0 + 32.0);
});
Here, celsius is a Signal<f64>, and fahrenheit is another Signal<f64>. The watch! macro registers a reactive dependency: whenever celsius changes, the closure is called with the new value, and the Fahrenheit value is recalculated and set.
Watching Multiple Signals
The watch! macro can observe multiple signals simultaneously. This is useful when a derived value depends on several inputs:
watch!(red, green, blue, |r, g, b| {
hex_color.set(format!("#{:02x}{:02x}{:02x}", r, g, b));
});
In this example, red, green, and blue are signals representing color channel values. The watch! macro monitors all three, and whenever any of them changes, the closure computes the corresponding hex color string and updates hex_color.
This pattern eliminates the need for manual event wiring and ensures that derived state is always consistent with its inputs.
Practical Patterns
Combining Props and Signals
In real applications, you will often combine the Props Down pattern with shared signals. A parent might pass some static configuration via props and share dynamic state via signals:
#[derive(Clone, Default)]
struct MyCardProps {
title: &'static str,
}
#[component]
pub fn my_card(node: VirtualNode<MyCardProps>) -> VirtualNode {
let MyCardProps { title, .. } = node.try_get_props().unwrap_or_default();
let children: VirtualNode = node.try_get_child_node();
html! {
div {
h3 { title }
children
}
}
}
The static title prop configures the card's appearance, while child components within the card can share signals for dynamic, interactive state.
Using Utility Functions
euv provides several utility functions that simplify common patterns:
-
use_toggle— A convenience hook for boolean toggle state, commonly used for modals, dropdowns, and expandable sections. -
on_input_value— An event handler that updates a signal from an input element's value. -
on_change_value— Similar toon_input_value, but foronChangeevents. -
on_change_checked— An event handler for checkboxonChangeevents, updating a signal with the checkbox's checked state.
These utilities reduce boilerplate and make component code more concise and readable.
Summary
euv's component binding system combines the best ideas from Rust's type system and modern reactive UI patterns:
- Props Down / Callback Up provides a clear, unidirectional data flow for most parent-child communication.
- Strongly typed props catch errors at compile time, eliminating runtime surprises.
- Custom callbacks enable flexible child-to-parent communication.
- Shared signals provide true bidirectional binding when needed.
-
watch!macro enables powerful cross-component reactive bindings, automatically keeping derived state in sync.
By combining these patterns, you can build complex, interactive UIs with confidence, knowing that the compiler and the reactive system have your back at every step.
Project Code:https://github.com/euv-dev/euv
Top comments (0)