Skip to main content

Overview

State management is how you handle data that changes over time in your application. Freya provides multiple approaches to state management, from simple local component state to sophisticated global state with fine-grained reactivity.
State in Freya is reactive - when state changes, components that read that state automatically re-render.

Available APIs

Freya offers three main approaches to state management:

Local State

use_state for component-specific data

Freya Radio

Global state with channels and fine-grained updates

Readable/Writable

Type-erased state for reusable components

Local State

Local state is managed with the use_state hook. It’s perfect for component-specific data like hover states, input values, or toggles.

Basic Usage

use freya::prelude::*;

#[derive(PartialEq)]
struct Counter;

impl Component for Counter {
    fn render(&self) -> impl IntoElement {
        let mut count = use_state(|| 0);

        rect()
            .child(format!("Count: {}", count.read()))
            .child(
                Button::new()
                    .on_press(move |_| {
                        *count.write() += 1;
                    })
                    .child("+")
            )
    }
}

Reading State

Use .read() to access the current value:
let count = use_state(|| 0);

// Read the value
let value = *count.read(); // Dereference to get i32

// Display in UI
rect().child(format!("Count: {}", count.read()))
Calling .read() subscribes the component to that state. The component will re-render whenever the state changes.

Writing State

Use .write() to get a mutable reference:
let mut count = use_state(|| 0);

// Increment
*count.write() += 1;

// Set directly
*count.write() = 10;

// Use .set() for convenience
count.set(10);

Passing State to Children

State values are Copy, so you can easily pass them to child components:
use freya::prelude::*;

fn app() -> impl IntoElement {
    let mut count = use_state(|| 0);

    rect()
        .child(Display { count })
        .child(Controls { count })
}

#[derive(PartialEq)]
struct Display {
    count: State<i32>,
}

impl Component for Display {
    fn render(&self) -> impl IntoElement {
        format!("Count: {}", self.count.read())
    }
}

#[derive(PartialEq)]
struct Controls {
    count: State<i32>,
}

impl Component for Controls {
    fn render(&self) -> impl IntoElement {
        rect()
            .horizontal()
            .spacing(8.0)
            .child(
                Button::new()
                    .on_press(move |_| *self.count.write() -= 1)
                    .child("-")
            )
            .child(
                Button::new()
                    .on_press(move |_| *self.count.write() += 1)
                    .child("+")
            )
    }
}
No need to .clone() state values - they implement Copy!

Global State with Freya Radio

For complex applications that need to share state across many components, Freya Radio provides a powerful global state management system with fine-grained reactivity.

Why Freya Radio?

Fine-Grained Updates

Components only re-render when their specific data changes

Channel-Based

Subscribe to specific slices of state

Multi-Window Support

Share state across multiple windows

Type-Safe

Compile-time guarantees with Rust’s type system

Setting Up Radio

1

Define your state

Create a struct to hold your application state:
#[derive(Default, Clone)]
struct AppState {
    count: i32,
    user_name: String,
}
2

Define channels

Channels let components subscribe to specific state changes:
#[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
enum AppChannel {
    Count,
    UserName,
}

impl RadioChannel<AppState> for AppChannel {}
3

Initialize the radio station

In your root component:
fn app() -> impl IntoElement {
    use_init_radio_station::<AppState, AppChannel>(AppState::default);
    
    rect().child(Counter {})
}

Using Radio in Components

use freya::prelude::*;
use freya::radio::*;

#[derive(PartialEq)]
struct Counter;

impl Component for Counter {
    fn render(&self) -> impl IntoElement {
        // Subscribe to the Count channel
        let mut radio = use_radio(AppChannel::Count);

        rect()
            .child(format!("Count: {}", radio.read().count))
            .child(
                Button::new()
                    .on_press(move |_| {
                        radio.write().count += 1;
                    })
                    .child("+")
            )
    }
}
Only components subscribed to a channel re-render when that channel’s data changes. This is much more efficient than global state that re-renders everything.

Multiple Channels Example

Channels allow different parts of your app to update independently:
use freya::prelude::*;
use freya::radio::*;

#[derive(Default, Clone)]
struct TodoState {
    todos: Vec<String>,
    filter: Filter,
}

#[derive(Clone, Default, PartialEq)]
enum Filter {
    #[default]
    All,
    Completed,
    Pending,
}

#[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
enum TodoChannel {
    Todos,
    Filter,
}

impl RadioChannel<TodoState> for TodoChannel {}

fn app() -> impl IntoElement {
    use_init_radio_station::<TodoState, TodoChannel>(TodoState::default);
    
    rect()
        .child(TodoList {})    // Only re-renders when todos change
        .child(FilterBar {})   // Only re-renders when filter changes
}

#[derive(PartialEq)]
struct TodoList;

impl Component for TodoList {
    fn render(&self) -> impl IntoElement {
        let todos = use_radio(TodoChannel::Todos);
        
        rect()
            .children(
                todos.read()
                    .todos
                    .iter()
                    .map(|todo| label().text(todo.clone()))
            )
    }
}

#[derive(PartialEq)]
struct FilterBar;

impl Component for FilterBar {
    fn render(&self) -> impl IntoElement {
        let mut radio = use_radio(TodoChannel::Filter);
        
        rect()
            .horizontal()
            .child(
                Button::new()
                    .on_press(move |_| radio.write().filter = Filter::All)
                    .child("All")
            )
            .child(
                Button::new()
                    .on_press(move |_| radio.write().filter = Filter::Completed)
                    .child("Completed")
            )
    }
}

Channel Derivation

Channels can notify other channels when they change:
impl RadioChannel<TodoState> for TodoChannel {
    fn derive_channel(self, _state: &TodoState) -> Vec<Self> {
        match self {
            TodoChannel::Todos => {
                // When todos change, also notify the filter channel
                vec![self, TodoChannel::Filter]
            }
            TodoChannel::Filter => vec![self],
        }
    }
}

Reducer Pattern

For complex state updates, implement the reducer pattern:
use freya::prelude::*;
use freya::radio::*;

#[derive(Clone)]
struct CounterState {
    count: i32,
}

#[derive(Clone)]
enum CounterAction {
    Increment,
    Decrement,
    Reset,
    Set(i32),
}

#[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
enum CounterChannel {
    Count,
}

impl RadioChannel<CounterState> for CounterChannel {}

impl DataReducer for CounterState {
    type Channel = CounterChannel;
    type Action = CounterAction;

    fn reduce(&mut self, action: CounterAction) -> ChannelSelection<CounterChannel> {
        match action {
            CounterAction::Increment => self.count += 1,
            CounterAction::Decrement => self.count -= 1,
            CounterAction::Reset => self.count = 0,
            CounterAction::Set(value) => self.count = value,
        }
        ChannelSelection::Current
    }
}

#[derive(PartialEq)]
struct Counter;

impl Component for Counter {
    fn render(&self) -> impl IntoElement {
        let mut radio = use_radio(CounterChannel::Count);

        rect()
            .child(format!("{}", radio.read().count))
            .child(
                Button::new()
                    .on_press(move |_| radio.apply(CounterAction::Increment))
                    .child("+")
            )
            .child(
                Button::new()
                    .on_press(move |_| radio.apply(CounterAction::Reset))
                    .child("Reset")
            )
    }
}

Multi-Window Applications

Share state across multiple windows:
use freya::prelude::*;
use freya::radio::*;

#[derive(Default, Clone)]
struct AppState {
    count: i32,
}

#[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
enum AppChannel {
    Count,
}

impl RadioChannel<AppState> for AppChannel {}

fn main() {
    // Create a global radio station
    let radio_station = RadioStation::create_global(AppState::default());

    launch(
        LaunchConfig::new()
            .with_window(WindowConfig::new_app(Window1 {
                radio_station: radio_station.clone(),
            }))
            .with_window(WindowConfig::new_app(Window2 {
                radio_station,
            }))
    );
}

struct Window1 {
    radio_station: RadioStation<AppState, AppChannel>,
}

impl App for Window1 {
    fn render(&self) -> impl IntoElement {
        use_share_radio(move || self.radio_station.clone());
        let mut radio = use_radio(AppChannel::Count);

        rect()
            .child(format!("Window 1: {}", radio.read().count))
            .child(
                Button::new()
                    .on_press(move |_| radio.write().count += 1)
                    .child("+")
            )
    }
}

struct Window2 {
    radio_station: RadioStation<AppState, AppChannel>,
}

impl App for Window2 {
    fn render(&self) -> impl IntoElement {
        use_share_radio(move || self.radio_station.clone());
        let radio = use_radio(AppChannel::Count);

        // Both windows share the same state!
        rect().child(format!("Window 2: {}", radio.read().count))
    }
}

Readable and Writable

For building reusable components that can accept state from any source, use Readable<T> and Writable<T>.

Writable State

Writable<T> allows components to accept state without knowing whether it’s local or global:
use freya::prelude::*;

#[derive(PartialEq)]
struct NameInput {
    name: Writable<String>,
}

impl Component for NameInput {
    fn render(&self) -> impl IntoElement {
        Input::new(self.name.clone())
    }
}

fn app() -> impl IntoElement {
    let local_name = use_state(|| "Alice".to_string());

    rect().child(NameInput {
        name: local_name.into_writable(), // Convert local state
    })
}

Readable State

Readable<T> is for read-only state:
#[derive(PartialEq)]
struct Display {
    value: Readable<i32>,
}

impl Component for Display {
    fn render(&self) -> impl IntoElement {
        format!("Value: {}", self.value.read())
    }
}

fn app() -> impl IntoElement {
    let count = use_state(|| 0);

    rect().child(Display {
        value: count.into_readable(), // Convert to readable
    })
}

Converting Between State Types

// Local state to Writable
let state = use_state(|| 0);
let writable = state.into_writable();

// Local state to Readable  
let readable = state.into_readable();

// Writable to Readable
let readable = Readable::from(writable);

// Radio slice to Writable
let radio = use_radio(AppChannel::Count);
let slice = radio.slice_mut_current(|s| &mut s.count);
let writable = slice.into_writable();

Choosing the Right Approach

Use Local State When...

  • State is only used in one component
  • You have simple, isolated data
  • Examples: hover state, form input, toggle

Use Freya Radio When...

  • State is shared across many components
  • You need fine-grained reactivity
  • Building a multi-window app
  • Performance is critical

Best Practices

Only lift state up to a common ancestor when multiple components need it:
// ❌ Bad - state too high in tree
fn app() -> impl IntoElement {
    let hover = use_state(|| false); // Only used in Button
    rect().child(Button { hover })
}

// ✅ Good - state in component that uses it
#[derive(PartialEq)]
struct Button;
impl Component for Button {
    fn render(&self) -> impl IntoElement {
        let hover = use_state(|| false);
        // Use hover state here
    }
}
Design your channels so components only subscribe to the data they need:
// ✅ Good - separate channels
enum AppChannel {
    UserName,   // Profile component subscribes
    TodoList,   // Todo component subscribes
    Theme,      // All UI components subscribe
}
This makes your components work with any state source:
#[derive(PartialEq)]
struct Counter {
    value: Writable<i32>, // Works with local or global state
}

Complete Example

Here’s a complete todo app using Freya Radio:
use freya::prelude::*;
use freya::radio::*;

fn main() {
    launch(LaunchConfig::new().with_window(WindowConfig::new(app)))
}

#[derive(Default, Clone)]
struct TodoState {
    todos: Vec<Todo>,
    input: String,
}

#[derive(Clone)]
struct Todo {
    text: String,
    completed: bool,
}

#[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
enum TodoChannel {
    Todos,
    Input,
}

impl RadioChannel<TodoState> for TodoChannel {}

fn app() -> impl IntoElement {
    use_init_radio_station::<TodoState, TodoChannel>(TodoState::default);

    rect()
        .padding(Gaps::new_all(20.))
        .child(AddTodo {})
        .child(TodoList {})
}

#[derive(PartialEq)]
struct AddTodo;

impl Component for AddTodo {
    fn render(&self) -> impl IntoElement {
        let mut radio = use_radio(TodoChannel::Input);

        rect()
            .horizontal()
            .spacing(8.0)
            .child(
                Input::new(radio.slice_mut_current(|s| &mut s.input).into_writable())
            )
            .child(
                Button::new()
                    .on_press(move |_| {
                        let text = radio.read().input.clone();
                        if !text.is_empty() {
                            radio.write().todos.push(Todo {
                                text,
                                completed: false,
                            });
                            radio.write().input.clear();
                        }
                    })
                    .child("Add")
            )
    }
}

#[derive(PartialEq)]
struct TodoList;

impl Component for TodoList {
    fn render(&self) -> impl IntoElement {
        let radio = use_radio(TodoChannel::Todos);

        rect()
            .children(
                radio.read()
                    .todos
                    .iter()
                    .enumerate()
                    .map(|(i, todo)| {
                        TodoItem {
                            index: i,
                            todo: todo.clone(),
                        }
                    })
            )
    }
}

#[derive(PartialEq)]
struct TodoItem {
    index: usize,
    todo: Todo,
}

impl Component for TodoItem {
    fn render(&self) -> impl IntoElement {
        let mut radio = use_radio(TodoChannel::Todos);
        let index = self.index;

        rect()
            .horizontal()
            .child(
                Checkbox::new(self.todo.completed)
                    .on_change(move |_| {
                        radio.write().todos[index].completed = !radio.read().todos[index].completed;
                    })
            )
            .child(label().text(&self.todo.text))
    }
}

Next Steps

Hooks

Learn about lifecycle hooks

Components

Build components with state

Events

Handle user interactions

Examples

See state management in action

Build docs developers (and LLMs) love