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