Back to flin
flin

Attribute Reactivity

How FLIN's fine-grained reactivity system tracks dependencies at the attribute level -- updating only the specific DOM attributes that change, not entire components.

Thales & Claude | March 25, 2026 9 min flin
flinreactivityattributesbinding

When you write

in FLIN, two things happen when active changes: the class attribute updates, and nothing else. The style attribute does not re-evaluate. The element is not destroyed and recreated. The div's children are not re-rendered. Only the specific attribute that depends on the changed variable is touched.

This is attribute-level reactivity -- FLIN's approach to making UI updates as cheap as possible. Session 253 refined this system to its final form, building on the reactivity engine (covered in article 028) to track dependencies not just at the component level, but at the individual attribute level.

The Problem With Component-Level Reactivity

Most frameworks operate at the component level. When a state variable changes, the entire component re-renders, producing a new virtual DOM tree. The framework then diffs the old and new trees to find what changed and patches the real DOM accordingly.

This works, but it does unnecessary work. Consider a component with 50 elements:

count = 0
active = false

Dashboard

{active ? "Active" : "Inactive"}
```

When count changes from 0 to 1, the only thing that needs to update is the Stat component's value prop. But in a framework with component-level reactivity, the entire dashboard re-renders: all 50 elements, all their attributes, all their children. The virtual DOM diff finds that only the Stat changed, but it had to compare everything to discover that.

FLIN skips the comparison. It knows, at compile time, exactly which attributes depend on which variables. When count changes, it updates only the attributes that reference count. Everything else is untouched.

How Attribute Tracking Works

During compilation, the emitter analyzes each attribute expression and records which variables it references:

<div
    class="card {if active then 'active' else ''}"
    style="opacity: {opacity}; transform: translateX({offset}px)"
    data-count={count}
>

The compiler produces a dependency map:

class  -> [active]
style  -> [opacity, offset]
data-count -> [count]

At runtime, each entry in this map becomes a "reactive binding." When active changes, only the class binding is re-evaluated. When opacity changes, only the style binding is re-evaluated. When count changes, only the data-count binding is re-evaluated.

pub struct ReactiveBinding {
    pub element_id: usize,
    pub attribute: String,
    pub expression: CompiledExpr,
    pub dependencies: Vec<VariableId>,
}

pub struct ReactiveScope { bindings: Vec, // Map from variable -> bindings that depend on it dependency_graph: HashMap>, }

impl ReactiveScope { pub fn notify_change(&mut self, var_id: VariableId, vm: &mut Vm) { if let Some(binding_ids) = self.dependency_graph.get(&var_id) { for &binding_id in binding_ids { let binding = &self.bindings[binding_id]; let new_value = vm.eval_expr(&binding.expression)?; dom_set_attribute(binding.element_id, &binding.attribute, &new_value); } } } } ```

The notify_change function is O(k) where k is the number of bindings that depend on the changed variable. For a typical component, k is 1-5. For a complex component with 50 elements, k is still 1-5 -- because most elements do not depend on any given variable.

Text Content Reactivity

The same tracking applies to text content:

count = 0
<p>You have clicked {count} times</p>

The text node "You have clicked {count} times" depends on count. When count changes, the text node's content is updated. The

element itself is not touched. Other text nodes in the component are not touched.

For components with many text nodes (a data table with 100 rows and 5 columns = 500 text nodes), this granularity is significant. Updating one cell's value touches one text node, not 500.

Two-Way Binding

Form inputs need two-way binding: the input displays a variable's value, and typing in the input updates the variable. FLIN handles this with a single value attribute:

name = ""
<input value={name} />
// Typing "Juste" updates name to "Juste"
// Setting name = "Claude" updates the input display

When the user types, FLIN's event system captures the input event and updates name. When name changes (from code), the reactive binding updates the input's value attribute.

This is simpler than React's controlled component pattern (which requires both value and onChange) and more explicit than Vue's v-model (which is syntactic sugar for the same thing). In FLIN, value={name} is a bidirectional binding by default for input elements.

The compiler detects that value on an ,

0/2000
Loading responses...

Related Articles