Lifecycle Hooks #

Components declare side effects through the lifecycle() method. The framework manages registration, execution, and cleanup automatically.

How lifecycle works #

The framework calls lifecycle() after every build and update. Each call collects a fresh set of effects — the framework clears old effects and installs the new ones. This means effects are always consistent with current props and state.

impl Component for Timer {
type State = TimerState;
fn lifecycle(&self, hooks: &mut Hooks<TimerState>, _state: &TimerState) {
if self.running {
hooks.use_interval(Duration::from_secs(1), |s| s.elapsed += 1);
}
hooks.use_mount(|s| s.started_at = Instant::now());
hooks.use_unmount(|s| println!("Timer ran for {:?}", s.started_at.elapsed()));
}
}

Notice that the interval is conditional — when self.running changes to false, the next lifecycle() call simply doesn't register the interval, and the framework stops it.

Available hooks #

use_interval #

Fires periodically at the given duration during the framework's tick cycle:

hooks.use_interval(Duration::from_millis(80), |state| {
state.frame = state.frame.wrapping_add(1);
});

The handler receives &mut State — any mutation automatically marks the component dirty for re-rendering. This is how the built-in Spinner animates: it registers an 80ms interval that cycles through Braille frames.

use_mount #

Fires once after the component is first built:

hooks.use_mount(|state| {
state.created_at = Instant::now();
state.entries.push("Component mounted".into());
});

Use this for one-time initialization that depends on state being available.

use_unmount #

Fires once when the component is removed from the tree:

hooks.use_unmount(|state| {
println!("Component lived for {:?}", state.created_at.elapsed());
});

Use this for cleanup: logging, recording metrics, etc. Note that the handler receives &mut State — you can still read state during unmount.

use_autofocus #

Requests focus when the component mounts:

hooks.use_autofocus();

If multiple components mount with autofocus in the same rebuild, the last one wins. This is typically used for input fields that should be focused on creation.

provide_context #

Makes a typed value available to all descendant components. See Context for details:

hooks.provide_context(self.theme.clone());

use_context #

Reads a value provided by an ancestor. See Context for details:

hooks.use_context::<Theme>(|theme, state| {
state.current_theme = theme.cloned();
});

Effect lifecycle #

Here's the full sequence when a component is built or updated:

  1. lifecycle() is called with a fresh Hooks instance
  2. The component registers its effects via the hooks API
  3. Old effects are cleared
  4. New effects are installed
  5. use_mount fires (only on first build)
  6. use_context handlers fire (after lifecycle() returns)

When a component is removed:

  1. use_unmount fires
  2. All effects (including intervals) are cleaned up
  3. The node is tombstoned and its slot freed for reuse

Conditional effects #

Because lifecycle() runs on every rebuild, you can conditionally register effects:

fn lifecycle(&self, hooks: &mut Hooks<MyState>, state: &MyState) {
// Only animate when visible
if self.visible {
hooks.use_interval(Duration::from_millis(100), |s| {
s.animation_frame += 1;
});
}
// Always track mount/unmount
hooks.use_mount(|s| s.log("mounted"));
hooks.use_unmount(|s| s.log("unmounted"));
}

When self.visible changes from true to false, the interval stops on the next rebuild. When it changes back, a new interval starts.

Example: StatusLog component #

This example from the lifecycle example shows a component that records its own lifecycle events:

struct StatusLog {
name: String,
}
#[derive(Default)]
struct StatusLogState {
entries: Vec<(String, Style)>,
}
impl Component for StatusLog {
type State = StatusLogState;
fn initial_state(&self) -> Option<StatusLogState> {
let mut state = StatusLogState::default();
state.entries.push((
format!(" {} created", self.name),
Style::default().fg(Color::DarkGray),
));
Some(state)
}
fn lifecycle(&self, hooks: &mut Hooks<StatusLogState>, _state: &StatusLogState) {
let mount_name = self.name.clone();
hooks.use_mount(move |state| {
state.entries.push((
format!(" {} mounted", mount_name),
Style::default().fg(Color::Green),
));
});
let unmount_name = self.name.clone();
hooks.use_unmount(move |state| {
state.entries.push((
format!(" {} unmounted", unmount_name),
Style::default().fg(Color::Red),
));
});
}
fn render(&self, area: Rect, buf: &mut Buffer, state: &Self::State) {
let lines: Vec<Line> = state.entries.iter()
.map(|(text, style)| Line::from(Span::styled(text.as_str(), *style)))
.collect();
Paragraph::new(lines).render(area, buf);
}
fn desired_height(&self, _width: u16, state: &Self::State) -> u16 {
state.entries.len() as u16
}
}

Hook reference #

HookFires whenReceives
use_interval(duration, handler)Periodically during tick cycle&mut State
use_mount(handler)Once, after first build&mut State
use_unmount(handler)Once, when component removed&mut State
use_autofocus()Requests focus on mount
provide_context(value)Makes value available to descendants
use_context::<T>(handler)After lifecycle returnsOption<&T>, &mut Tracked<S>