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