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:
- Persisting state between renders
- Triggering re-renders when state changes
- 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 changespeek()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 Memo | Don’t Use Memo |
|---|---|
| Expensive calculations | Simple arithmetic |
| Filtering/sorting large lists | Formatting strings |
| Derived data from state | Constants |
| Computed values used in multiple places | One-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:
- Scrolling to an element after it appears
- Focusing an input after it mounts
- Measurements that need the rendered DOM
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_effecthandles 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:
- Lazy loading images
- Starting animations when visible
- Tracking analytics (element viewed)
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
| Hook | Purpose |
|---|---|
use_state | Local reactive state |
use_memo | Cached computed values |
use_reactive | Make props reactive |
use_side_effect | Run code on state changes |
use_effect | Async side effects |
use_focus | Focus management |
use_scroll | Scroll control |
use_visible | Visibility tracking |
use_provide_context | Provide data to descendants |
use_consume | Access ancestor context |
use_try_consume | Optional context access |
Summary
In this tutorial, you learned:
- What hooks are and why they’re necessary for reactivity
- The rules of hooks - only at top level, only in components
- use_state - creating and managing local state
- use_memo - caching expensive computations
- use_reactive - making props reactive
- use_side_effect - reacting to state changes
- use_effect - async side effects
- use_focus - focus management
- use_scroll - programmatic scrolling
- use_visible - visibility tracking
- Context hooks - sharing data across components
- Custom hooks - combining hooks for reusable logic
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.