freya/_docs/
state_management.rs

1//! # State Management
2//!
3//! Freya provides several options for managing state in your applications.
4//! This guide covers local state with [`use_state`](crate::prelude::use_state) and global state with **Freya Radio**.
5//!
6//! ## Local State
7//!
8//! Local state is managed with the [`use_state`](crate::prelude::use_state) hook.
9//! It's perfect for component-specific state that doesn't need to be shared.
10//!
11//! ```rust,no_run
12//! # use freya::prelude::*;
13//! #[derive(PartialEq)]
14//! struct Counter {}
15//!
16//! impl Component for Counter {
17//!     fn render(&self) -> impl IntoElement {
18//!         let mut count = use_state(|| 0);
19//!
20//!         rect().child(format!("Count: {}", *count.read())).child(
21//!             Button::new()
22//!                 .on_press(move |_| *count.write() += 1)
23//!                 .child("+"),
24//!         )
25//!     }
26//! }
27//! ```
28//!
29//! ## Global State with Freya Radio 🧬
30//!
31//! For complex applications that need to share state across multiple components,
32//! Freya Radio provides a powerful global state management system with fine-grained reactivity.
33//!
34//! ### Key Concepts
35//!
36//! - **RadioStation**: The central hub that holds the global state and manages subscriptions.
37//! - **RadioChannel**: Defines channels for subscribing to specific types of state changes.
38//! - **Radio**: A reactive handle to the state for a specific channel.
39//!
40//! ### Basic Usage
41//!
42//! First, define your state type and channels:
43//!
44//! ```rust,no_run
45//! # use freya::prelude::*;
46//! # use freya::radio::*;
47//! #[derive(Default, Clone)]
48//! struct AppState {
49//!     count: i32,
50//! }
51//!
52//! #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
53//! enum AppChannel {
54//!     Count,
55//! }
56//!
57//! impl RadioChannel<AppState> for AppChannel {}
58//! ```
59//!
60//! Then, initialize the radio station and use it in components:
61//!
62//! ```rust,no_run
63//! # use freya::prelude::*;
64//! # use freya::radio::*;
65//! # #[derive(Default, Clone)]
66//! # struct AppState { count: i32 }
67//! #
68//! # #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
69//! # enum AppChannel { Count }
70//! #
71//! # impl RadioChannel<AppState> for AppChannel {}
72//! fn app() -> impl IntoElement {
73//!     // Initialize the radio station
74//!     use_init_radio_station::<AppState, AppChannel>(AppState::default);
75//!
76//!     rect().child(Counter {})
77//! }
78//!
79//! #[derive(PartialEq)]
80//! struct Counter {}
81//!
82//! impl Component for Counter {
83//!     fn render(&self) -> impl IntoElement {
84//!         // Subscribe to the Count channel
85//!         let mut radio = use_radio(AppChannel::Count);
86//!
87//!         rect()
88//!             .child(format!("Count: {}", radio.read().count))
89//!             .child(
90//!                 Button::new()
91//!                     .on_press(move |_| radio.write().count += 1)
92//!                     .child("+"),
93//!             )
94//!     }
95//! }
96//! ```
97//!
98//! ### Multiple Channels
99//!
100//! You can use multiple channels for different types of updates:
101//!
102//! ```rust,no_run
103//! # use freya::prelude::*;
104//! # use freya::radio::*;
105//! #[derive(Default, Clone)]
106//! struct TodoState {
107//!     todos: Vec<String>,
108//!     filter: Filter,
109//! }
110//!
111//! #[derive(Clone, Default)]
112//! enum Filter {
113//!     #[default]
114//!     All,
115//!     Completed,
116//!     Pending,
117//! }
118//!
119//! #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
120//! enum TodoChannel {
121//!     AddTodo,
122//!     ToggleTodo(usize),
123//!     ChangeFilter,
124//! }
125//!
126//! impl RadioChannel<TodoState> for TodoChannel {
127//!     fn derive_channel(self, _state: &TodoState) -> Vec<Self> {
128//!         match self {
129//!             TodoChannel::AddTodo | TodoChannel::ToggleTodo(_) => {
130//!                 vec![self, TodoChannel::ChangeFilter] // Also notify filter subscribers
131//!             }
132//!             TodoChannel::ChangeFilter => vec![self],
133//!         }
134//!     }
135//! }
136//!
137//! fn app() -> impl IntoElement {
138//!     use_init_radio_station::<TodoState, TodoChannel>(TodoState::default);
139//!
140//!     rect().child(TodoList {}).child(FilterSelector {})
141//! }
142//!
143//! #[derive(PartialEq)]
144//! struct TodoList {}
145//!
146//! impl Component for TodoList {
147//!     fn render(&self) -> impl IntoElement {
148//!         let todos = use_radio(TodoChannel::AddTodo);
149//!
150//!         rect().child(format!("Todos: {}", todos.read().todos.len()))
151//!     }
152//! }
153//!
154//! #[derive(PartialEq)]
155//! struct FilterSelector {}
156//!
157//! impl Component for FilterSelector {
158//!     fn render(&self) -> impl IntoElement {
159//!         let mut radio = use_radio(TodoChannel::ChangeFilter);
160//!
161//!         rect()
162//!             .child(
163//!                 Button::new()
164//!                     .on_press(move |_| radio.write().filter = Filter::All)
165//!                     .child("All"),
166//!             )
167//!             .child(
168//!                 Button::new()
169//!                     .on_press(move |_| radio.write().filter = Filter::Completed)
170//!                     .child("Completed"),
171//!             )
172//!     }
173//! }
174//! ```
175//!
176//! ### Multi-Window Applications
177//!
178//! For applications with multiple windows, use a global radio station:
179//!
180//! ```rust,no_run
181//! # use freya::prelude::*;
182//! # use freya::radio::*;
183//! #[derive(Default, Clone)]
184//! struct AppState {
185//!     count: i32,
186//! }
187//!
188//! #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
189//! enum AppChannel {
190//!     Count,
191//! }
192//!
193//! impl RadioChannel<AppState> for AppChannel {}
194//!
195//! fn main() {
196//!     let radio_station = RadioStation::create_global(AppState::default());
197//!
198//!     launch(
199//!         LaunchConfig::new()
200//!             .with_window(WindowConfig::new_app(Window1 { radio_station }))
201//!             .with_window(WindowConfig::new_app(Window2 { radio_station })),
202//!     );
203//! }
204//!
205//! struct Window1 {
206//!     radio_station: RadioStation<AppState, AppChannel>,
207//! }
208//!
209//! impl App for Window1 {
210//!     fn render(&self) -> impl IntoElement {
211//!         use_share_radio(move || self.radio_station);
212//!         let mut radio = use_radio(AppChannel::Count);
213//!
214//!         rect()
215//!             .child(format!("Window 1: {}", radio.read().count))
216//!             .child(
217//!                 Button::new()
218//!                     .on_press(move |_| radio.write().count += 1)
219//!                     .child("+"),
220//!             )
221//!     }
222//! }
223//!
224//! struct Window2 {
225//!     radio_station: RadioStation<AppState, AppChannel>,
226//! }
227//!
228//! impl App for Window2 {
229//!     fn render(&self) -> impl IntoElement {
230//!         use_share_radio(move || self.radio_station);
231//!         let radio = use_radio(AppChannel::Count);
232//!
233//!         rect().child(format!("Window 2: {}", radio.read().count))
234//!     }
235//! }
236//! ```
237//!
238//! ### Reducers
239//!
240//! For complex state updates, implement the reducer pattern:
241//!
242//! ```rust,no_run
243//! # use freya::prelude::*;
244//! # use freya::radio::*;
245//! #[derive(Clone)]
246//! struct CounterState {
247//!     count: i32,
248//! }
249//!
250//! #[derive(Clone)]
251//! enum CounterAction {
252//!     Increment,
253//!     Decrement,
254//!     Set(i32),
255//! }
256//!
257//! #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
258//! enum CounterChannel {
259//!     Count,
260//! }
261//!
262//! impl RadioChannel<CounterState> for CounterChannel {}
263//!
264//! impl DataReducer for CounterState {
265//!     type Channel = CounterChannel;
266//!     type Action = CounterAction;
267//!
268//!     fn reduce(&mut self, action: CounterAction) -> ChannelSelection<CounterChannel> {
269//!         match action {
270//!             CounterAction::Increment => self.count += 1,
271//!             CounterAction::Decrement => self.count -= 1,
272//!             CounterAction::Set(value) => self.count = value,
273//!         }
274//!         ChannelSelection::Current
275//!     }
276//! }
277//!
278//! #[derive(PartialEq)]
279//! struct CounterComponent {}
280//!
281//! impl Component for CounterComponent {
282//!     fn render(&self) -> impl IntoElement {
283//!         let mut radio = use_radio(CounterChannel::Count);
284//!
285//!         rect()
286//!             .child(
287//!                 Button::new()
288//!                     .on_press(move |_| {
289//!                         radio.apply(CounterAction::Increment);
290//!                     })
291//!                     .child("+"),
292//!             )
293//!             .child(format!("{}", radio.read().count))
294//!             .child(
295//!                 Button::new()
296//!                     .on_press(move |_| {
297//!                         radio.apply(CounterAction::Decrement);
298//!                     })
299//!                     .child("-"),
300//!             )
301//!     }
302//! }
303//! ```
304//!
305//! ## Readable and Writable interfaces
306//!
307//! Freya provides [`Readable<T>`](crate::prelude::Readable) and [`Writable<T>`](crate::prelude::Writable)
308//! as type-erased abstractions over different state sources. These allow components to accept state
309//! without knowing whether it comes from local state (`use_state`) or global state (Freya Radio).
310//!
311//! ### Writable
312//!
313//! [`Writable<T>`](crate::prelude::Writable) is for state that can be both read and written to.
314//! Components like [`Input`](crate::components::Input) accept `Writable` values, allowing you to
315//! pass any state source that can be converted to a `Writable`.
316//!
317//! Sources that can be converted to `Writable`:
318//! - [`State<T>`](crate::prelude::State) from `use_state` via [`IntoWritable`](crate::prelude::IntoWritable)
319//! - [`RadioSliceMut`](freya_radio::prelude::RadioSliceMut) from Freya Radio via [`IntoWritable`](crate::prelude::IntoWritable)
320//!
321//! ```rust,no_run
322//! # use freya::prelude::*;
323//! # use freya_radio::prelude::*;
324//! # #[derive(Default, Clone)]
325//! # struct AppState { name: String }
326//! #
327//! # #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
328//! # enum AppChannel { Name }
329//! #
330//! # impl RadioChannel<AppState> for AppChannel {}
331//!
332//! #[derive(PartialEq)]
333//! struct NameInput {
334//!     name: Writable<String>,
335//! }
336//!
337//! impl Component for NameInput {
338//!     fn render(&self) -> impl IntoElement {
339//!         // Can read and write to the state
340//!         Input::new(self.name.clone())
341//!     }
342//! }
343//!
344//! fn app() -> impl IntoElement {
345//!     use_init_radio_station::<AppState, AppChannel>(AppState::default);
346//!
347//!     let local_name = use_state(|| "Alice".to_string());
348//!     let radio = use_radio(AppChannel::Name);
349//!     let name_slice = radio.slice_mut_current(|s| &mut s.name);
350//!
351//!     rect()
352//!         // Pass local state as Writable
353//!         .child(NameInput {
354//!             name: local_name.into_writable(),
355//!         })
356//!         // Pass radio slice as Writable
357//!         .child(NameInput {
358//!             name: name_slice.into_writable(),
359//!         })
360//! }
361//! ```
362//!
363//! ### Readable
364//!
365//! [`Readable<T>`](crate::prelude::Readable) is for read-only state. It's the same concept as
366//! `Writable` but only exposes read operations. This is useful when a component only needs to
367//! display data without modifying it.
368//!
369//! Sources that can be converted to `Readable`:
370//! - [`State<T>`](crate::prelude::State) from `use_state` via [`IntoReadable`](crate::prelude::IntoReadable)
371//! - [`RadioSlice`](freya_radio::prelude::RadioSlice) from Freya Radio via [`IntoReadable`](crate::prelude::IntoReadable)
372//! - [`Writable<T>`](crate::prelude::Writable) via [`From<Writable<T>>`](crate::prelude::Readable)
373//!
374//! ```rust,no_run
375//! # use freya::prelude::*;
376//! # use freya_radio::prelude::*;
377//! # #[derive(Default, Clone)]
378//! # struct AppState { count: i32 }
379//! #
380//! # #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
381//! # enum AppChannel { Count }
382//! #
383//! # impl RadioChannel<AppState> for AppChannel {}
384//!
385//! #[derive(PartialEq)]
386//! struct Counter {
387//!     count: Readable<i32>,
388//! }
389//!
390//! impl Component for Counter {
391//!     fn render(&self) -> impl IntoElement {
392//!         // Can only read the value
393//!         format!("Count: {}", self.count.read())
394//!     }
395//! }
396//!
397//! fn app() -> impl IntoElement {
398//!     use_init_radio_station::<AppState, AppChannel>(AppState::default);
399//!
400//!     let local_count = use_state(|| 0);
401//!     let radio = use_radio(AppChannel::Count);
402//!     let count_slice = radio.slice_current(|s| &s.count);
403//!
404//!     rect()
405//!         // Pass local state as Readable
406//!         .child(Counter {
407//!             count: local_count.into_readable(),
408//!         })
409//!         // Pass radio slice as Readable
410//!         .child(Counter {
411//!             count: count_slice.into_readable(),
412//!         })
413//! }
414//! ```
415//!
416//! ## Choosing Between Local and Global State
417//!
418//! - **Use local state** (`use_state`) for:
419//!   - Component-specific data
420//!   - Simple state that doesn't need precise updates
421//!
422//! - **Use Freya Radio** for:
423//!   - Application-wide state
424//!   - Complex state logic with multiple subscribers
425//!   - Apps that require precise updates for max performance
426//!   - Multi-window applications
427//!
428//! ## Examples
429//!
430//! Check out these examples in the repository:
431//!
432//! - [`state_radio.rs`](https://github.com/marc2332/freya/tree/main/examples/state_radio.rs) - Basic radio usage
433//! - [`feature_tray_radio_state.rs`](https://github.com/marc2332/freya/tree/main/examples/feature_tray_radio_state.rs) - Tray integration
434//! - [`feature_multi_window_radio_state.rs`](https://github.com/marc2332/freya/tree/main/examples/feature_multi_window_radio_state.rs) - Multi-window state sharing