Part 5 of 14 19 Jan 2026 By Raj Patil 35 min read

Part 5: Hooks - The Heart of Reactivity

Part 5 of the Freya Rust GUI series. Master hooks to add state, effects, and reactivity to your components. Learn use_state, use_memo, use_effect, and more.

Intermediate #rust #freya #gui #hooks #reactivity #state #tutorial
Building Native GUIs with Rust & Freya 5 / 14

Hooks

Hooks are special functions that let you add state and side effects to your components. They’re the secret sauce that makes Freya UIs reactive - automatically updating when data changes.

[!NOTE] What is Reactivity? Reactivity means the UI automatically updates when your data changes. You don’t manually update the screen - you just change the data, and Freya handles the rest. This is achieved through hooks.


Understanding Hooks

The Problem Without Hooks

Imagine you want to track a counter:

// This WON'T work properly
fn counter() -> impl IntoElement {
    let count = 0;  // Local variable - resets on every render!
    
    rect()
        .child(label().text(format!("Count: {}", count)))
        .child(
            Button::new()
                .on_press(|_| {
                    // How do we modify count? We can't!
                })
                .child("+")
        )
}

The problem: count is a local variable that’s recreated every time the component renders. And we can’t modify it from inside the button’s event handler.

The Solution: Hooks

Hooks solve this by:

  1. Persisting state between renders
  2. Triggering re-renders when state changes
  3. Allowing modification from event handlers
fn counter() -> impl IntoElement {
    let mut count = use_state(|| 0);  // State persists!
    
    rect()
        .child(label().text(format!("Count: {}", *count.read())))
        .child(
            Button::new()
                .on_press(move |_| *count.write() += 1)  // Modify state!
                .child("+")
        )
}

The Rules of Hooks

Hooks are not ordinary functions - they have special rules you must follow:

Rule 1: Only Call Hooks at the Top Level

Hooks must always be called in the same order on every render.

// ❌ WRONG - Hook inside a condition
fn component() -> impl IntoElement {
    let show_count = true;
    
    if show_count {
        let count = use_state(|| 0);  // ERROR! Hook order changes
    }
}

// ✅ CORRECT - Hook at top level
fn component() -> impl IntoElement {
    let show_count = true;
    let count = use_state(|| 0);  // Always called in same order
    
    if show_count {
        // Use count here
    }
}

Rule 2: Only Call Hooks Inside Component Render

Hooks cannot be called in regular functions, event handlers, or outside components.

// ❌ WRONG - Hook in event handler
fn component() -> impl IntoElement {
    rect()
        .on_pointer_down(|_| {
            let state = use_state(|| 0);  // ERROR!
        })
}

// ✅ CORRECT - Hook at component level
fn component() -> impl IntoElement {
    let state = use_state(|| 0);
    
    rect()
        .on_pointer_down(move |_| {
            state.set(1);  // Use the state
        })
}

[!WARNING] Why These Rules? Freya tracks hooks by their call order. If you call hooks conditionally or in loops, the order changes between renders, and Freya loses track of which state belongs to which hook.


use_state: Managing Local State

use_state creates reactive local state for your component.

Basic Usage

fn component() -> impl IntoElement {
    // Create state with initial value
    let mut count = use_state(|| 0);
    
    // Read the value
    let value = *count.read();
    
    // Write to the value
    *count.write() += 1;
    
    // Set a new value
    count.set(5);
}

Reading State

let count = use_state(|| 0);

// Method 1: read() - subscribes to changes (recommended)
let value = *count.read();

// Method 2: peek() - reads without subscribing
let value = *count.peek();

// Method 3: Call syntax (for Copy types)
let value = count();  // Same as *count.read()

[!TIP] read() vs peek()

  • read() tells Freya “this component depends on this state” - it will re-render when the state changes
  • peek() reads the value without creating a dependency - use when you need the value but don’t want to trigger re-renders

Writing State

let mut count = use_state(|| 0);

// Method 1: Direct write access
*count.write() += 1;
*count.write() = 10;

// Method 2: set() method
count.set(5);

// Method 3: set_if_modified() - only updates if value changed
count.set_if_modified(5);  // Won't trigger re-render if already 5

// Method 4: with_mut() for complex modifications
count.with_mut(|mut v| {
    *v += 1;
    *v *= 2;
});

Boolean Toggle Pattern

For true/false states:

let mut visible = use_state(|| false);

visible.toggle();              // Flip true/false
let new_value = visible.toggled();  // Toggle and return new value

if visible() {
    // Show something
}

Option State Pattern

For optional values:

let mut maybe_data = use_state(|| None::<String>);

// Set a value
maybe_data.set(Some("Hello".to_string()));

// Take ownership (sets to None)
let data = maybe_data.take();

// Check if has value
if let Some(value) = maybe_data.peek() {
    // Use value
}

Collection State Pattern

For vectors and collections:

let mut items = use_state(|| Vec::<String>::new());

// Add item
items.with_mut(|mut v| {
    v.push("New item".to_string());
});

// Remove item
items.with_mut(|mut v| {
    v.retain(|item| item != "Remove me");
});

// Clear all
items.set(Vec::new());

Complete Counter Example

fn counter() -> impl IntoElement {
    let mut count = use_state(|| 0);
    
    rect()
        .direction(Direction::Vertical)
        .gap(16.0)
        .main_align_center()
        .cross_align_center()
        .child(
            label()
                .text(format!("Count: {}", count()))
                .font_size(48.0)
        )
        .child(
            rect()
                .direction(Direction::Horizontal)
                .gap(8.0)
                .child(
                    Button::new()
                        .on_press(move |_| *count.write() -= 1)
                        .child("-")
                )
                .child(
                    Button::new()
                        .on_press(move |_| count.set(0))
                        .child("Reset")
                )
                .child(
                    Button::new()
                        .on_press(move |_| *count.write() += 1)
                        .child("+")
                )
        )
}

use_memo: Cached Computed Values

use_memo creates a cached value that only recomputes when dependencies change.

Why Use Memo?

fn expensive_list() -> impl IntoElement {
    let items = use_state(|| vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
    let threshold = use_state(|| 5);
    
    // This filter runs on EVERY render, even if items/threshold didn't change
    let filtered: Vec<i32> = items.read()
        .iter()
        .filter(|&&x| x > *threshold.read())
        .copied()
        .collect();
    
    // ...
}

With use_memo, the filter only runs when items or threshold changes:

fn expensive_list() -> impl IntoElement {
    let items = use_state(|| vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
    let threshold = use_state(|| 5);
    
    // Only recomputes when items or threshold changes
    let filtered = use_memo(move || {
        items.read()
            .iter()
            .filter(|&&x| x > *threshold.read())
            .copied()
            .collect::<Vec<i32>>()
    });
    
    // Access the cached value
    let result = filtered.read();
    
    // ...
}

Basic Usage

let count = use_state(|| 5);

let doubled = use_memo(move || {
    count() * 2
});

let value = *doubled.read();  // Or doubled()

Multiple Dependencies

let a = use_state(|| 10);
let b = use_state(|| 20);

let sum = use_memo(move || {
    a() + b()  // Recomputes when either a or b changes
});

let product = use_memo(move || {
    a() * b()
});

When to Use Memo

Use MemoDon’t Use Memo
Expensive calculationsSimple arithmetic
Filtering/sorting large listsFormatting strings
Derived data from stateConstants
Computed values used in multiple placesOne-time calculations

use_reactive: Making Props Reactive

use_reactive converts a borrowed value (like a prop) into reactive state.

The Problem

When you pass data to a component as props, changes to that data don’t automatically trigger re-renders.

The Solution

#[derive(PartialEq)]
struct UserProfile {
    config: Config,
}

impl Component for UserProfile {
    fn render(&self) -> impl IntoElement {
        // Convert prop to reactive state
        let config = use_reactive(&self.config);
        
        // Now we can use it in effects
        use_side_effect(move || {
            let cfg = config.read();
            println!("Config changed: {:?}", cfg);
        });
        
        rect()
            .child(label().text(config.read().name.clone()))
    }
}

use_side_effect: Reacting to Changes

use_side_effect runs code whenever tracked state changes.

Basic Usage

fn component() -> impl IntoElement {
    let count = use_state(|| 0);
    
    // Runs whenever count changes
    use_side_effect(move || {
        println!("Count is now: {}", count());
    });
    
    rect()
        .child(Button::new()
            .on_press(move |_| *count.write() += 1)
            .child("Increment")
        )
}

Effect with Dependencies

let user_id = use_state(|| 1);

// Only runs when user_id changes
use_side_effect(move || {
    println!("User ID changed to: {}", user_id());
    // Fetch user data, etc.
});

After Side Effect

Run effects after the render instead of before:

use_after_side_effect(|| {
    println!("This runs after the UI is rendered");
});

This is useful for:

Effect with Return Value

let computed = use_side_effect_value(|| {
    // Expensive one-time computation
    perform_heavy_calculation()
});

use_effect: Async Side Effects

For async operations like API calls:

fn user_profile() -> impl IntoElement {
    let user_id = use_state(|| 1);
    let user_data = use_state(|| None::<User>);
    
    // Runs when user_id changes
    use_effect(move || {
        let user_id = user_id();
        let user_data = user_data.clone();
        
        async move {
            let user = fetch_user(user_id).await;
            user_data.set(Some(user));
        }
    });
    
    match user_data.peek() {
        Some(user) => rect().child(label().text(&user.name)),
        None => rect().child(label().text("Loading...")),
    }
}

[!NOTE] Async Effects Freya’s use_effect handles async closures. The effect re-runs when any state read inside it changes.


use_focus: Focus Management

use_focus helps manage keyboard focus for accessibility and interactivity.

Basic Usage

fn focusable_element() -> impl IntoElement {
    let focus = use_focus();
    
    rect()
        .a11y_id(focus.a11y_id())
        .a11y_focusable(true)
        .on_pointer_down(move |_| {
            focus.request_focus();
        })
        .child(label().text("Click to focus"))
}

Checking Focus Status

fn styled_button() -> impl IntoElement {
    let focus = use_focus();
    let focus_status = use_focus_status(focus);
    
    let border_color = match focus_status() {
        FocusStatus::Keyboard => Color::BLUE,    // Focused via Tab
        FocusStatus::Pointer => Color::GREEN,    // Focused via click
        FocusStatus::Not => Color::GRAY,         // Not focused
    };
    
    rect()
        .a11y_id(focus.a11y_id())
        .a11y_focusable(true)
        .border(Border::new().width(2.0).fill(border_color))
        .padding(8.0)
        .child(label().text("Focusable"))
}

Focus Methods

let focus = use_focus();

// Request focus
focus.request_focus();

// Remove focus
focus.request_unfocus();

// Check if focused
if focus.is_focused() {
    // Element has focus
}

// Check if focused by keyboard
if focus.is_focused_with_keyboard() {
    // Focused via Tab navigation
}

use_scroll: Scroll Control

use_scroll lets you control scroll position programmatically.

Basic Usage

fn scrollable_list() -> impl IntoElement {
    let scroll = use_scroll();
    
    rect()
        .height(Size::px(400.0))
        .overflow(Overflow::Scroll)
        .scroll_handle(scroll)
        .child(long_content())
        .child(
            Button::new()
                .on_press(move |_| {
                    scroll.scroll_to(ScrollPosition::Top);
                })
                .child("Scroll to Top")
        )
}

Scroll Positions

// Scroll to top
scroll.scroll_to(ScrollPosition::Top);

// Scroll to bottom
scroll.scroll_to(ScrollPosition::Bottom);

// Scroll to specific offset
scroll.scroll_to(ScrollPosition::Offset(100.0));

// Get current position
let position = scroll.get_scroll_position();

use_visible: Visibility Tracking

use_visible tracks whether an element is visible on screen.

fn lazy_loaded() -> impl IntoElement {
    let visibility = use_visible();
    
    rect()
        .reference(&visibility.node_ref())
        .child(if visibility.is_visible() {
            expensive_content()
        } else {
            placeholder()
        })
}

This is useful for:


Context Hooks

Context lets you share data across components without passing props.

Providing Context

fn parent() -> impl IntoElement {
    // Provide context for all descendants
    use_provide_context(|| Theme::Dark);
    
    // All children can access this theme
    ChildComponent {}
}

Consuming Context

fn child() -> impl IntoElement {
    // Access context from ancestors
    let theme = use_consume::<Theme>();
    
    rect()
        .background(theme.background_color())
        .child(label().text("Themed!"))
}

Optional Context

When context might not exist:

fn child() -> impl IntoElement {
    if let Some(theme) = use_try_consume::<Theme>() {
        // Theme is available
    } else {
        // No theme provided, use default
    }
}

Custom Hooks

You can create your own hooks by combining existing ones.

Toggle Hook

fn use_toggle(initial: bool) -> State<bool> {
    let mut value = use_state(|| initial);
    value
}

// Usage
let visible = use_toggle(false);
visible.toggle();

Counter Hook

fn use_counter(initial: i32) -> (Readable<i32>, impl Fn(i32)) {
    let mut count = use_state(|| initial);
    let set_count = move |delta: i32| *count.write() += delta;
    (count.into_readable(), set_count)
}

// Usage
let (count, adjust) = use_counter(0);
adjust(1);   // Increment
adjust(-1);  // Decrement

Form Input Hook

fn use_input(initial: &str) -> (State<String>, impl FnMut(String)) {
    let mut value = use_state(|| initial.to_string());
    let setter = move |new_value: String| value.set(new_value);
    (value, setter)
}

// Usage
let (name, set_name) = use_input("");
set_name("John".to_string());

Hooks Summary Table

HookPurpose
use_stateLocal reactive state
use_memoCached computed values
use_reactiveMake props reactive
use_side_effectRun code on state changes
use_effectAsync side effects
use_focusFocus management
use_scrollScroll control
use_visibleVisibility tracking
use_provide_contextProvide data to descendants
use_consumeAccess ancestor context
use_try_consumeOptional context access

Summary

In this tutorial, you learned:


Previous: Part 4: Styling ←

Next: Part 6: State Management →

In the next tutorial, we’ll explore advanced state management with Freya Radio - how to share state across your entire application and between windows.