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
// 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) }
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
// ChatWindow.flin
messages = []
ws = noneonMount { // 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
// 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} }
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
// LiveDashboard.flin
interval_id = none
event_source = noneonMount { 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:onUnmountruns 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 removedSteps 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
// UserProfile.flin
user = none
loading = true
error = noneonMount { 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}
Keyboard Shortcuts
// 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
// StockTicker.flin
prices = []
refresh_interval = noneonMount { 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} }
Scroll Position Restoration
// InfiniteList.flin
items = []
scroll_pos = 0onMount { 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:
pub 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:
// 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:
onMount {
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:
| onMount | onUnmount |
|---|---|
| Start timer | Clear timer |
| Open WebSocket | Close WebSocket |
| Add event listener | Remove event listener |
| Start polling | Stop polling |
| Subscribe to store | Unsubscribe 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