Back to flin
flin

The Component Lifecycle

How FLIN components manage their lifecycle -- onMount for initialization, onUpdate for reactions, onUnmount for cleanup -- with a simpler model than React or Vue.

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

Every component has a life. It is created, it appears on the screen, it reacts to changes, and eventually it is removed. The question for a component framework is: how much of this lifecycle does the developer need to manage?

React has useEffect with its dependency arrays and cleanup functions. Vue has onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount, and onActivated. Angular has ngOnInit, ngOnChanges, ngDoCheck, ngAfterContentInit, ngAfterContentChecked, ngAfterViewInit, ngAfterViewChecked, and ngOnDestroy.

Session 035 designed FLIN's component lifecycle with three hooks: onMount, onUpdate, and onUnmount. Three. Not eight. Not twelve. Three hooks that cover every real-world use case, because every other lifecycle event is either redundant or harmful.

The Three Hooks

flin// Dashboard.flin

// Called once when the component first appears
onMount {
    print("Dashboard mounted")
    data = fetch_dashboard_data()
    timer = set_interval(60.seconds, refresh_data)
}

// Called when reactive dependencies change
onUpdate {
    print("Dashboard updated -- data changed")
}

// Called when the component is removed
onUnmount {
    print("Dashboard unmounted -- cleaning up")
    clear_interval(timer)
}

<div class="dashboard">
    {if data != none}
        <Grid cols={3} gap={4}>
            {for metric in data.metrics}
                <Stat label={metric.label} value={metric.value} />
            {/for}
        </Grid>
    {else}
        <Spinner />
    {/if}
</div>

onMount: Component Initialization

onMount runs once, after the component's initial render. This is the place for:

  • Fetching data from APIs
  • Starting timers and intervals
  • Setting up event listeners (keyboard shortcuts, window resize)
  • Initializing third-party integrations
flin// ChatWindow.flin
messages = []
ws = none

onMount {
    // Fetch message history
    response = http_get("/api/messages")
    {if response.ok}
        messages = response.json
    {/if}

    // Connect WebSocket for real-time updates
    ws = ws_connect("/ws/chat")
    ws.on_message(msg => {
        messages = messages.push(parse_json(msg))
    })

    // Scroll to bottom
    scroll_to_bottom()
}

onMount is semantically equivalent to React's useEffect(() => { ... }, []) (empty dependency array) or Vue's onMounted(() => { ... }). The difference is simplicity: no dependency array to forget, no hook ordering rules, no rules of hooks.

onUpdate: Reacting to Changes

onUpdate runs after every re-render caused by a state change. It is the place for:

  • Side effects that should happen when data changes
  • DOM measurements after a render (scrolling, focusing)
  • Analytics tracking when displayed data changes
flin// SearchResults.flin
query = props.query
results = []

onUpdate {
    // Re-fetch when query changes
    {if query != ""}
        response = http_get("/api/search?q={query}")
        {if response.ok}
            results = response.json
        {/if}
    {/if}
}

<div>
    <Text>{results.len} results for "{query}"</Text>
    {for result in results}
        <SearchResult item={result} />
    {/for}
</div>

onUpdate is called after the render, not before. This means the DOM is already updated when the hook runs. If you need to measure element dimensions or scroll positions, they reflect the current state.

Unlike React's useEffect (which runs for every render unless you specify a dependency array), FLIN's onUpdate is coalesced -- if multiple state changes happen in the same event loop tick, onUpdate runs once after all changes are applied.

onUnmount: Cleanup

onUnmount runs once, just before the component is removed from the DOM. This is the place for:

  • Clearing timers and intervals
  • Closing connections (WebSocket, EventSource)
  • Removing event listeners
  • Canceling pending requests
flin// LiveDashboard.flin
interval_id = none
event_source = none

onMount {
    interval_id = set_interval(30.seconds, refresh_data)
    event_source = new EventSource("/api/events")
    event_source.on_message(handle_event)
}

onUnmount {
    {if interval_id != none}
        clear_interval(interval_id)
    {/if}
    {if event_source != none}
        event_source.close()
    {/if}
}

Every resource acquired in onMount should be released in onUnmount. This symmetry prevents memory leaks. FLIN does not enforce it (a timer that is not cleared will continue to fire after the component is removed), but the three-hook structure makes the pattern obvious: set up in onMount, clean up in onUnmount.

Why Only Three Hooks

React, Vue, and Angular offer many more lifecycle hooks. Here is why FLIN does not:

"Before" Hooks Are Unnecessary

Vue's onBeforeMount, onBeforeUpdate, and onBeforeUnmount run before the corresponding events. In practice, they are rarely used because:

  • onBeforeMount: anything you would do here, you can do in the component's top-level code (which runs before the first render).
  • onBeforeUpdate: FLIN's reactive system handles update batching. If you need to read the "old" state before an update, store it in a variable.
  • onBeforeUnmount: onUnmount runs before the DOM is actually removed. There is no meaningful distinction.

Content/View Init Hooks Are Framework-Specific

Angular's ngAfterContentInit and ngAfterViewInit exist because Angular has a content projection system (ng-content) that initializes separately from the component's own template. FLIN's slot system (covered in the next article) does not have this two-phase initialization, so these hooks are unnecessary.

"Check" Hooks Are Performance Antipatterns

Angular's ngDoCheck and ngAfterContentChecked are called on every change detection cycle. They are performance traps -- code inside them runs on every mouse move, every keystroke, every scroll event. FLIN's fine-grained reactivity system means change detection is automatic and targeted. There is nothing to "check."

The Lifecycle in Sequence

A component's full lifecycle:

1. Component referenced in parent template (<Dashboard />)
2. Component file found in registry
3. Component source compiled to bytecode
4. Component's top-level code executes (variable initialization)
5. First render produces HTML
6. HTML inserted into DOM
7. onMount runs                          <- First lifecycle hook
8. User interacts / data changes
9. Reactive system detects change
10. Re-render produces updated HTML
11. DOM patched with changes
12. onUpdate runs                        <- Runs after each re-render
13. ... (steps 8-12 repeat)
14. Parent removes component (condition becomes false, navigation changes)
15. onUnmount runs                       <- Last lifecycle hook
16. DOM elements removed

Steps 1-6 happen once. Steps 8-12 repeat as many times as needed. Steps 14-16 happen once. The developer controls behavior at three points: initialization (onMount), reaction (onUpdate), and cleanup (onUnmount).

Real-World Patterns

Data Fetching with Loading State

flin// UserProfile.flin
user = none
loading = true
error = none

onMount {
    response = http_get("/api/users/{props.user_id}")
    {if response.ok}
        user = response.json
    {else}
        error = "Failed to load user"
    {/if}
    loading = false
}

{if loading}
    <Skeleton variant="card" />
{else if error != none}
    <Alert variant="danger">{error}</Alert>
{else}
    <Card>
        <Avatar name={user.name} size="xl" />
        <Text size="xl" weight="bold">{user.name}</Text>
        <Text color="muted">{user.email}</Text>
    </Card>
{/if}

Keyboard Shortcuts

flin// Editor.flin
onMount {
    on_keydown(event => {
        {if event.ctrl and event.key == "s"}
            event.prevent_default()
            save_document()
        {/if}
        {if event.ctrl and event.key == "z"}
            event.prevent_default()
            undo()
        {/if}
    })
}

onUnmount {
    off_keydown()  // Remove keyboard listener
}

Auto-Refresh with Cleanup

flin// StockTicker.flin
prices = []
refresh_interval = none

onMount {
    fetch_prices()
    refresh_interval = set_interval(5.seconds, fetch_prices)
}

onUnmount {
    clear_interval(refresh_interval)
}

fn fetch_prices() {
    response = http_get("/api/stocks")
    {if response.ok}
        prices = response.json
    {/if}
}

<Table data={prices} columns={[
    { key: "symbol", label: "Symbol" },
    { key: "price", label: "Price", format: "currency" },
    { key: "change", label: "Change" }
]} />

Scroll Position Restoration

flin// InfiniteList.flin
items = []
scroll_pos = 0

onMount {
    load_items()
    // Restore scroll position if returning to this page
    scroll_to(scroll_pos)
}

onUpdate {
    // Save scroll position when items change
    scroll_pos = get_scroll_position()
}

onUnmount {
    // Save for next visit
    save_scroll_position(scroll_pos)
}

Implementation: Hook Registration

The lifecycle hooks are registered during component compilation. The compiler identifies onMount { ... }, onUpdate { ... }, and onUnmount { ... } blocks and stores them as named closures in the compiled component:

rustpub struct CompiledComponent {
    pub name: String,
    pub template: Vec<ViewNode>,
    pub styles: Option<String>,
    pub on_mount: Option<Closure>,
    pub on_update: Option<Closure>,
    pub on_unmount: Option<Closure>,
    pub top_level_code: Vec<u8>,  // Bytecode for variable initialization
}

During rendering, the VM calls these closures at the appropriate times:

rust// After first render and DOM insertion
if let Some(mount) = &component.on_mount {
    vm.call_closure(mount)?;
}

// After each re-render
if let Some(update) = &component.on_update {
    vm.call_closure(update)?;
}

// Before removing from DOM
if let Some(unmount) = &component.on_unmount {
    vm.call_closure(unmount)?;
}

The closures capture the component's scope, so they have access to all local variables (including props). This is the same closure mechanism used by lambda expressions in higher-order functions -- no special magic, just standard lexical scoping.

Three Hooks, Complete Control

React's hook system is powerful but complex. The "rules of hooks" (must be called at the top level, must be called in the same order, must not be called inside conditions) are a source of constant confusion and bugs. Vue's Composition API is simpler but still has eight lifecycle hooks. Angular's lifecycle interface requires implementing interfaces and remembering method names.

FLIN's three hooks cover every real use case. Initialize in onMount. React in onUpdate. Clean up in onUnmount. The names are obvious. The behavior is predictable. The pattern is symmetric. There is nothing else to learn.

Error Handling in Lifecycle Hooks

Errors inside lifecycle hooks are caught and reported without crashing the entire application:

flinonMount {
    response = http_get("/api/data")
    {if not response.ok}
        // This error is logged but does not crash the component
        log_error("Failed to load data: {response.status}")
        error = "Failed to load data"
    {/if}
}

If an onMount hook throws an unhandled error, FLIN logs the error with the component name and file location, and the component renders without the hook's side effects. The parent component and sibling components are not affected. This isolation prevents a single failing API call from taking down an entire page.

The same isolation applies to onUpdate and onUnmount. An error in one component's lifecycle does not propagate to other components. Each component is an isolated unit of behavior, and its lifecycle errors are its own.

The Symmetry Principle

The three-hook system follows a symmetry principle: every resource acquired in onMount should be released in onUnmount. This creates a clean mental model:

onMountonUnmount
Start timerClear timer
Open WebSocketClose WebSocket
Add event listenerRemove event listener
Start pollingStop polling
Subscribe to storeUnsubscribe from store

When a developer writes onMount, they should immediately write the corresponding onUnmount. This prevents the most common source of memory leaks in component-based applications: resources that are acquired when a component appears but never released when it disappears.

The symmetry is not enforced by the compiler (FLIN does not verify that every onMount has a matching onUnmount), but the three-hook structure makes the pattern obvious. With React's useEffect, the cleanup function is a returned closure buried inside the effect -- easy to forget. With FLIN's onUnmount, the cleanup is a top-level block that mirrors onMount -- hard to forget.


This is Part 90 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed a three-hook component lifecycle that covers every real-world use case.

Series Navigation: - [89] Scoped CSS and Computed Styles - [90] The Component Lifecycle (you are here) - [91] Slots and Content Projection - [92] Attribute Reactivity

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles