freya/_docs/
state_management.rs

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