
Element Level Reactivity: Momenta's Design Philosophy
Sometime in 2023, I embarked on a journey to migrate my blogfolio from its current NextJs stack to a Rust based stack. There's no specific reason for doing this. I love NextJS but I love Rust more. I wanted to show to myself how much of a rust fanatic I really am lol. In any case, things didn't work out as expected.
Call me crazy but I love how predictable react renders are. It's as simple as: State Change → Component Re-renders → DOM Updates. There's something weirdly comforting about being able to trace through what happens. But React gets a lot of hate for all the re-rendering. This lead me to build something that keeps React's execution model but updates at the element level instead of component level.
After looking into Leptos, SolidJS, and React, I developed Element Level Reactivity. It's not trying to be the fastest - it's trying to be the most practical. I built an implementation called Momenta in Rust.
The core technical difference
Fine-grained reactivity tracks dependencies at the expression level and creates direct signal-to-DOM connections:
// Leptos can do this - only the text node updates
#[component]
fn Counter() -> impl IntoView {
let (count, set_count) = signal(0);
view! {
<div>
<p>"Count: " {count}</p> // Surgical update, "Count: " is not updated
<button on:click=move |_| set_count.update(|n| *n += 1)>
"Increment"
</button>
</div>
}
}
Element Level Reactivity tracks dependencies at the component level. When a signal changes, only the components/elements that read that signal re-render - not their parents:
// Momenta - component tracks count, re-renders target element
#[component]
fn Counter() -> Node {
let mut count = create_signal(0);
rsx! {
<div> // no re-render happens to this div element
<p>Count: {count}</p> // this entire p element re-renders
<button on_click={move |_| count += 1}>Increment</button> // no re-render
<button on_click={move |_| count -= 1}>Decrement</button> // no re-render
</div>
}
}
The example above may be a bit complex to understand but the gist is easily understood if you see html elements as components in momenta. And each component is isolated from its parents. This isolation is key - parent components don't re-render when child components update. Each component is its own reactive boundary.
Component-level dependency tracking
The dependency tracking happens at the component level, not globally:
#[component]
fn Counter() -> Node {
let mut count = create_signal(0);
let mut name = create_signal("");
// this component itself doesn't track any signal since it doesn't read a signal
rsx! {
<div>
<p>Count: {count}</p> // starts tracking count
<button on_click={move |_| count += 1}>Increment</button>
// button below tracks `name` but not `count`
<button on_click={move |_| count -= 1}>Decrement: {name}</button>
</div>
}
}
This is way simpler than fine-grained systems that need to track individual reactive computations and manage effect queues.
Each component execution rebuilds its dependency graph from scratch. This gives you automatic dependency cleanup. When a component re-renders, its dependencies are rebuilt based on what signals it actually reads during that render.
Effect execution model
All effects within a component re-run when the component re-renders, even if they didn't access the changing signal:
#[component]
fn UserProfile() -> Node {
let user_id = create_signal(1);
let user_name = create_signal(String::new());
// This effect re-runs whenever the component re-renders
// even if only user_name changed
// this effect also re-runs if user_id changes but not if user_name changes
create_effect(move || {
log_user_activity(user_id.get());
});
// This effect also re-runs on any component re-render
create_effect(move || {
update_page_title(&user_name.get());
});
rsx! {
<div>
<h1>{user_name}</h1>
<p>User ID: {user_id}</p>
</div>
}
}
This is different from fine-grained systems where effects only re-run if their specific dependencies change. In Momenta, all effects in a component are tied to that component's render cycle.
Parent-child update isolation
One of the biggest advantages is that parent components don't re-render when children update:
#[component]
fn App() -> Node {
let app_title = create_signal("My App".to_string());
rsx! {
<div>
<h1>{app_title}</h1> // Only re-renders if app_title changes
<Counter /> // Counter updates don't affect App
<UserProfile /> // UserProfile updates don't affect App
</div>
}
}
#[component]
fn Counter() -> Node {
let count = create_signal(0);
// When count changes, only this component re-renders
// App component is completely unaffected
rsx! {
<div>
<p>Count: {count}</p>
<button on_click={move |_| count += 1}>"++"</button>
</div>
}
}
React would re-render the entire App component tree when count changes. Momenta only re-renders the Counter component.
Conditional rendering advantages
Element Level Reactivity handles conditional rendering naturally because components re-execute:
#[component]
fn ConditionalWidget() -> Node {
let user_type = create_signal(UserType::Basic);
let loading = create_signal(false);
rsx! {
<div>
{if loading.get() {
// Only depends on loading signal when this branch executes
rsx! { <LoadingSpinner /> }
} else {
match user_type.get() {
UserType::Admin => {
// This component only depends on admin-specific signals
// when user is actually an admin
rsx! { <AdminPanel /> }
}
UserType::Premium => {
rsx! { <PremiumFeatures /> }
}
UserType::Basic => {
rsx! { <BasicProfile /> }
}
}
}}
</div>
}
}
The dependency graph automatically adjusts based on which branch executes. Fine-grained systems need explicit primitives to handle this.
Memory management
Momenta uses component-based ownership where everything dies when the component unmounts:
// When component unmounts, everything associated with it dies:
// - The component function
// - All signals created in this component
// - All effects in this component
// - All dependency tracking links
This eliminates subscription leaks and makes memory reasoning straightforward.
Technical performance characteristics
Update Granularity: Individual components (elements) rather than entire component trees
Time Complexity:
-
Signal access: O(1)
-
Signal update: O(k) where k = number of components that read the signal
-
Component rendering: O(1) per component
Space Complexity:
-
O(C × D) where C = components, D = average dependencies per component
-
Automatic cleanup prevents unbounded growth
The key insight is that component-level granularity hits the sweet spot - most UI updates affect entire logical units (form fields, list items, widgets) rather than individual text nodes.
Error handling advantages
Momenta gives you component-level error boundaries:
#[component]
fn RiskyComponent() -> Node {
// Component logic that might panic
let data = create_signal(risky_computation());
rsx! { <DataDisplay data={data} /> }
}
#[component]
fn SafeComponent() -> Node {
let result = std::panic::catch_unwind(|| {
rsx!(<RiskyComponent />)
});
match result {
Ok(element) => element,
Err(_) => rsx! { <ErrorFallback /> }
}
}
Fine-grained systems struggle with error boundaries because errors can happen in reactive computations that don't map to UI boundaries.
Integration with imperative code
Momenta works well with imperative DOM manipulation because components have stable identity within their update cycle:
#[component]
fn ChartComponent() -> Node {
let chart_data = create_signal(ChartData::default());
create_effect(move || {
// This runs after the component renders/re-renders
let chart_element = get_element_by_id("chart");
d3_update_chart(chart_element, chart_data.get());
});
rsx! {
<div id="chart" class="chart-container">
// D3 will manipulate this element imperatively
</div>
}
}
The component gets re-rendered when chart_data changes, but the DOM element is stable for the imperative library to work with.
Real-world examples
Dashboard widget with multiple signals:
#[component]
fn SystemWidget() -> Node {
let cpu_usage = create_signal(0.0);
let memory_usage = create_signal(0.0);
let disk_usage = create_signal(0.0);
// Any of these signals changing causes target elements to re-render
// but parent dashboard component is unaffected
rsx! {
<div class="system-widget"> // not re-rendered
<div class="metric">CPU: {cpu_usage.get():.1}%</div> // re-rendered
<div class="metric">Memory: {memory_usage.get():.1}%</div> // re-rendered
<div class="metric">Disk: {disk_usage.get():.1}%</div> // re-rendered
</div>
}
}
Form field with validation:
#[component]
fn EmailField() -> Node {
let email = create_signal(String::new());
let is_valid = create_signal(true);
// Validation effect runs on every re-render
create_effect(move || {
is_valid.set(email.get().contains('@'));
});
rsx! {
<div class={when!(is_valid => "field" else "field error")}>
<input
value={email}
on_input={move |e| email.set(e.target.value)}
/>
{when!(!is_valid => <span class="error">Invalid email</span>)}
</div>
}
}
The technical sweet spot
Element Level Reactivity with Momenta sits in a technical sweet spot:
-
Simpler than fine-grained: No reactive schedulers or complex dependency management
-
More efficient than coarse-grained: Updates only affected components, not entire trees
-
Familiar mental model: Components re-execute, just like React
-
Component isolation: Parents don't re-render when children update
-
Automatic memory management: Component-based cleanup eliminates leaks
-
Natural conditional handling: Dependencies adjust automatically based on execution
It's not the absolute fastest possible approach, but it's fast enough while being dramatically simpler to implement and reason about than fine-grained alternatives.
Try it out today - http://crates.io/crates/momenta
Comments: 0
Be the first to comment on this article!