dioxus_radio/hooks/
use_radio.rs

1use std::{
2    collections::{HashMap, HashSet},
3    hash::Hash,
4    ops::{Deref, DerefMut},
5    sync::{Arc, Mutex},
6};
7
8use dioxus::prelude::*;
9use dioxus_core::ReactiveContext;
10
11#[cfg(feature = "tracing")]
12pub trait RadioChannel<T>: 'static + PartialEq + Eq + Clone + Hash + std::fmt::Debug + Ord {
13    fn derive_channel(self, _radio: &T) -> Vec<Self> {
14        vec![self]
15    }
16}
17
18#[cfg(not(feature = "tracing"))]
19pub trait RadioChannel<T>: 'static + PartialEq + Eq + Clone + Hash {
20    fn derive_channel(self, _radio: &T) -> Vec<Self> {
21        vec![self]
22    }
23}
24
25/// Holds a global state and all its subscribers.
26pub struct RadioStation<Value, Channel>
27where
28    Channel: RadioChannel<Value>,
29    Value: 'static,
30{
31    value: Signal<Value>,
32    listeners: Signal<HashMap<Channel, Arc<Mutex<HashSet<ReactiveContext>>>>>,
33}
34
35impl<Value, Channel> Clone for RadioStation<Value, Channel>
36where
37    Channel: RadioChannel<Value>,
38{
39    fn clone(&self) -> Self {
40        *self
41    }
42}
43
44impl<Value, Channel> Copy for RadioStation<Value, Channel> where Channel: RadioChannel<Value> {}
45
46impl<Value, Channel> RadioStation<Value, Channel>
47where
48    Channel: RadioChannel<Value>,
49{
50    pub(crate) fn is_listening(
51        &self,
52        channel: &Channel,
53        reactive_context: &ReactiveContext,
54    ) -> bool {
55        let listeners = self.listeners.peek_unchecked();
56        listeners
57            .get(channel)
58            .map(|contexts| contexts.lock().unwrap().contains(reactive_context))
59            .unwrap_or_default()
60    }
61
62    pub(crate) fn listen(&self, channel: Channel, reactive_context: ReactiveContext) {
63        let mut listeners = self.listeners.write_unchecked();
64        let listeners = listeners.entry(channel).or_default();
65        reactive_context.subscribe(listeners.clone());
66    }
67
68    pub(crate) fn notify_listeners(&self, channel: &Channel) {
69        let listeners = self.listeners.write_unchecked();
70
71        #[cfg(feature = "tracing")]
72        tracing::info!("Notifying {channel:?}");
73
74        for (listener_channel, listeners) in listeners.iter() {
75            if listener_channel == channel {
76                for reactive_context in listeners.lock().unwrap().iter() {
77                    reactive_context.mark_dirty();
78                }
79            }
80        }
81    }
82
83    /// Read the current state value. This effectively subscribes to any change no matter the channel.
84    ///
85    /// Example:
86    ///
87    /// ```rs
88    /// let value = radio.read();
89    /// ```
90    pub fn read(&self) -> ReadableRef<'_, Signal<Value>> {
91        self.value.read()
92    }
93
94    /// Read the current state value without subscribing.
95    ///
96    /// Example:
97    ///
98    /// ```rs
99    /// let value = radio.peek();
100    /// ```
101    pub fn peek(&self) -> ReadableRef<'_, Signal<Value>> {
102        self.value.peek()
103    }
104
105    pub fn cleanup(&self) {
106        let mut listeners = self.listeners.write_unchecked();
107
108        // Clean up those channels with no reactive contexts
109        listeners.retain(|_, listeners| !listeners.lock().unwrap().is_empty());
110
111        #[cfg(feature = "tracing")]
112        {
113            use itertools::Itertools;
114            use tracing::{info, span, Level};
115
116            let mut channels_subscribers = HashMap::<&Channel, usize>::new();
117
118            for (channel, listeners) in listeners.iter() {
119                *channels_subscribers.entry(&channel).or_default() =
120                    listeners.lock().unwrap().len();
121            }
122
123            let span = span!(Level::DEBUG, "Radio Station Metrics");
124            let _enter = span.enter();
125
126            for (channel, count) in channels_subscribers.iter().sorted() {
127                info!(" {count} subscribers for {channel:?}")
128            }
129        }
130    }
131}
132
133pub struct RadioAntenna<Value, Channel>
134where
135    Channel: RadioChannel<Value>,
136    Value: 'static,
137{
138    pub(crate) channel: Channel,
139    station: RadioStation<Value, Channel>,
140}
141
142impl<Value, Channel> RadioAntenna<Value, Channel>
143where
144    Channel: RadioChannel<Value>,
145{
146    pub(crate) fn new(
147        channel: Channel,
148        station: RadioStation<Value, Channel>,
149    ) -> RadioAntenna<Value, Channel> {
150        RadioAntenna { channel, station }
151    }
152}
153
154pub struct RadioGuard<Value, Channel>
155where
156    Channel: RadioChannel<Value>,
157    Value: 'static,
158{
159    antenna: Signal<RadioAntenna<Value, Channel>>,
160    channels: Vec<Channel>,
161    value: WritableRef<'static, Signal<Value>>,
162}
163
164impl<Value, Channel> Drop for RadioGuard<Value, Channel>
165where
166    Channel: RadioChannel<Value>,
167{
168    fn drop(&mut self) {
169        for channel in &mut self.channels {
170            self.antenna.peek().station.notify_listeners(channel)
171        }
172        if !self.channels.is_empty() {
173            self.antenna.peek().station.cleanup();
174        }
175    }
176}
177
178impl<Value, Channel> Deref for RadioGuard<Value, Channel>
179where
180    Channel: RadioChannel<Value>,
181{
182    type Target = WritableRef<'static, Signal<Value>>;
183
184    fn deref(&self) -> &Self::Target {
185        &self.value
186    }
187}
188
189impl<Value, Channel> DerefMut for RadioGuard<Value, Channel>
190where
191    Channel: RadioChannel<Value>,
192{
193    fn deref_mut(&mut self) -> &mut WritableRef<'static, Signal<Value>> {
194        &mut self.value
195    }
196}
197
198/// `Radio` lets you access the state and is subscribed given it's `Channel`.
199pub struct Radio<Value, Channel>
200where
201    Channel: RadioChannel<Value>,
202    Value: 'static,
203{
204    antenna: Signal<RadioAntenna<Value, Channel>>,
205}
206
207impl<Value, Channel> Clone for Radio<Value, Channel>
208where
209    Channel: RadioChannel<Value>,
210{
211    fn clone(&self) -> Self {
212        *self
213    }
214}
215impl<Value, Channel> Copy for Radio<Value, Channel> where Channel: RadioChannel<Value> {}
216
217impl<Value, Channel> PartialEq for Radio<Value, Channel>
218where
219    Channel: RadioChannel<Value>,
220{
221    fn eq(&self, other: &Self) -> bool {
222        self.antenna == other.antenna
223    }
224}
225
226impl<Value, Channel> Radio<Value, Channel>
227where
228    Channel: RadioChannel<Value>,
229{
230    pub(crate) fn new(antenna: Signal<RadioAntenna<Value, Channel>>) -> Radio<Value, Channel> {
231        Radio { antenna }
232    }
233
234    pub(crate) fn subscribe_if_not(&self) {
235        if let Some(rc) = ReactiveContext::current() {
236            let antenna = &self.antenna.write_unchecked();
237            let channel = antenna.channel.clone();
238            let is_listening = antenna.station.is_listening(&channel, &rc);
239
240            // Subscribe the reader reactive context to the channel if it wasn't already
241            if !is_listening {
242                antenna.station.listen(channel, rc);
243            }
244        }
245    }
246
247    /// Read the current state value.
248    ///
249    /// Example:
250    ///
251    /// ```rs
252    /// let value = radio.read();
253    /// ```
254    pub fn read(&self) -> ReadableRef<'_, Signal<Value>> {
255        self.subscribe_if_not();
256        self.antenna.peek().station.value.peek_unchecked()
257    }
258
259    /// Read the current state value inside a callback.
260    ///
261    /// Example:
262    ///
263    /// ```rs
264    /// radio.with(|value| {
265    ///     // Do something with `value`
266    /// });
267    /// ```
268    pub fn with(&self, cb: impl FnOnce(ReadableRef<Signal<Value>>)) {
269        self.subscribe_if_not();
270        let value = self.antenna.peek().station.value;
271        let borrow = value.read();
272        cb(borrow);
273    }
274
275    /// Modify the state using the channel this radio was created with.
276    ///
277    /// Example:
278    ///
279    /// ```rs
280    /// radio.write().value = 1;
281    /// ```
282    pub fn write(&mut self) -> RadioGuard<Value, Channel> {
283        let value = self.antenna.peek().station.value.write_unchecked();
284        let channel = self.antenna.peek().channel.clone();
285        RadioGuard {
286            channels: channel.derive_channel(&*value),
287            antenna: self.antenna,
288            value,
289        }
290    }
291
292    /// Get a mutable reference to the current state value, inside a callback.
293    ///
294    /// Example:
295    ///
296    /// ```rs
297    /// radio.write_with(|value| {
298    ///     // Modify `value`
299    /// });
300    /// ```
301    pub fn write_with(&mut self, cb: impl FnOnce(RadioGuard<Value, Channel>)) {
302        let guard = self.write();
303        cb(guard);
304    }
305
306    /// Modify the state using a custom Channel.
307    ///
308    /// ## Example:
309    /// ```rs, no_run
310    /// radio.write(Channel::Whatever).value = 1;
311    /// ```
312    pub fn write_channel(&mut self, channel: Channel) -> RadioGuard<Value, Channel> {
313        let value = self.antenna.peek().station.value.write_unchecked();
314        RadioGuard {
315            channels: channel.derive_channel(&*value),
316            antenna: self.antenna,
317            value,
318        }
319    }
320
321    /// Get a mutable reference to the current state value, inside a callback.
322    ///
323    /// Example:
324    ///
325    /// ```rs
326    /// radio.write_channel_with(Channel::Whatever, |value| {
327    ///     // Modify `value`
328    /// });
329    /// ```
330    pub fn write_channel_with(
331        &mut self,
332        channel: Channel,
333        cb: impl FnOnce(RadioGuard<Value, Channel>),
334    ) {
335        let guard = self.write_channel(channel);
336        cb(guard);
337    }
338
339    /// Get a mutable reference to the current state value, inside a callback that returns the channel to be used.
340    ///
341    /// Example:
342    ///
343    /// ```rs
344    /// radio.write_with_channel_selection(|value| {
345    ///     // Modify `value`
346    ///     if value.cool {
347    ///         ChannelSelection::Select(Channel::Whatever)
348    ///     } else {
349    ///         ChannelSelection::Silence
350    ///     }
351    /// });
352    /// ```
353    pub fn write_with_channel_selection(
354        &mut self,
355        cb: impl FnOnce(&mut Value) -> ChannelSelection<Channel>,
356    ) -> ChannelSelection<Channel> {
357        let value = self.antenna.peek().station.value.write_unchecked();
358        let mut guard = RadioGuard {
359            channels: Vec::default(),
360            antenna: self.antenna,
361            value,
362        };
363        let channel_selection = cb(&mut guard.value);
364        let channel = match channel_selection.clone() {
365            ChannelSelection::Current => Some(self.antenna.peek().channel.clone()),
366            ChannelSelection::Silence => None,
367            ChannelSelection::Select(c) => Some(c),
368        };
369        if let Some(channel) = channel {
370            for channel in channel.derive_channel(&guard.value) {
371                self.antenna.peek().station.notify_listeners(&channel)
372            }
373            self.antenna.peek().station.cleanup();
374        }
375
376        channel_selection
377    }
378
379    /// Modify the state silently, no component will be notified.
380    ///
381    /// This is not recommended, the only intended usage for this is inside [RadioAsyncReducer].
382    ///
383    pub fn write_silently(&mut self) -> RadioGuard<Value, Channel> {
384        let value = self.antenna.peek().station.value.write_unchecked();
385        RadioGuard {
386            channels: Vec::default(),
387            antenna: self.antenna,
388            value,
389        }
390    }
391}
392
393impl<Channel> Copy for ChannelSelection<Channel> where Channel: Copy {}
394
395#[derive(Clone)]
396pub enum ChannelSelection<Channel> {
397    /// Notify the channel associated with the used [Radio].
398    Current,
399    /// Notify a given [Channel].
400    Select(Channel),
401    /// No subscriber will be notified.
402    Silence,
403}
404
405impl<Channel> ChannelSelection<Channel> {
406    /// Change to [ChannelSelection::Current]
407    pub fn current(&mut self) {
408        *self = Self::Current
409    }
410
411    /// Change to [ChannelSelection::Select]
412    pub fn select(&mut self, channel: Channel) {
413        *self = Self::Select(channel)
414    }
415
416    /// Change to [ChannelSelection::Silence]
417    pub fn silence(&mut self) {
418        *self = Self::Silence
419    }
420
421    /// Check if it is of type [ChannelSelection::Current]
422    pub fn is_current(&self) -> bool {
423        matches!(self, Self::Current)
424    }
425
426    /// Check if it is of type [ChannelSelection::Select] and return the channel.
427    pub fn is_select(&self) -> Option<&Channel> {
428        match self {
429            Self::Select(channel) => Some(channel),
430            _ => None,
431        }
432    }
433
434    /// Check if it is of type [ChannelSelection::Silence]
435    pub fn is_silence(&self) -> bool {
436        matches!(self, Self::Silence)
437    }
438}
439
440/// Consume the state and subscribe using the given `channel`
441/// Any mutation using this radio will notify other subscribers to the same `channel`,
442/// unless you explicitely pass a custom channel using other methods as [`Radio::write_channel()`]
443pub fn use_radio<Value, Channel>(channel: Channel) -> Radio<Value, Channel>
444where
445    Channel: RadioChannel<Value>,
446    Value: 'static,
447{
448    let station = use_context::<RadioStation<Value, Channel>>();
449
450    let mut radio = use_hook(|| {
451        let antenna = RadioAntenna::new(channel.clone(), station);
452        Radio::new(Signal::new(antenna))
453    });
454
455    if radio.antenna.peek().channel != channel {
456        radio.antenna.write().channel = channel;
457    }
458
459    radio
460}
461
462pub fn use_init_radio_station<Value, Channel>(
463    init_value: impl FnOnce() -> Value,
464) -> RadioStation<Value, Channel>
465where
466    Channel: RadioChannel<Value>,
467    Value: 'static,
468{
469    use_context_provider(|| RadioStation {
470        value: Signal::new(init_value()),
471        listeners: Signal::default(),
472    })
473}
474
475pub fn use_radio_station<Value, Channel>() -> RadioStation<Value, Channel>
476where
477    Channel: RadioChannel<Value>,
478    Value: 'static,
479{
480    use_context::<RadioStation<Value, Channel>>()
481}
482
483pub trait DataReducer {
484    type Channel;
485    type Action;
486
487    fn reduce(&mut self, action: Self::Action) -> ChannelSelection<Self::Channel>;
488}
489
490pub trait RadioReducer {
491    type Action;
492    type Channel;
493
494    fn apply(&mut self, action: Self::Action) -> ChannelSelection<Self::Channel>;
495}
496
497impl<
498        Data: DataReducer<Channel = Channel, Action = Action>,
499        Channel: RadioChannel<Data>,
500        Action,
501    > RadioReducer for Radio<Data, Channel>
502{
503    type Action = Action;
504    type Channel = Channel;
505
506    fn apply(&mut self, action: Action) -> ChannelSelection<Channel> {
507        self.write_with_channel_selection(|data| data.reduce(action))
508    }
509}
510
511pub trait DataAsyncReducer {
512    type Channel;
513    type Action;
514
515    #[allow(async_fn_in_trait)]
516    async fn async_reduce(
517        _radio: &mut Radio<Self, Self::Channel>,
518        _action: Self::Action,
519    ) -> ChannelSelection<Self::Channel>
520    where
521        Self::Channel: RadioChannel<Self>,
522        Self: Sized;
523}
524
525pub trait RadioAsyncReducer {
526    type Action;
527
528    fn async_apply(&mut self, _action: Self::Action)
529    where
530        Self::Action: 'static;
531}
532
533impl<
534        Data: DataAsyncReducer<Channel = Channel, Action = Action>,
535        Channel: RadioChannel<Data>,
536        Action,
537    > RadioAsyncReducer for Radio<Data, Channel>
538{
539    type Action = Action;
540
541    fn async_apply(&mut self, action: Self::Action)
542    where
543        Self::Action: 'static,
544    {
545        let mut radio = *self;
546        spawn(async move {
547            let channel = Data::async_reduce(&mut radio, action).await;
548            radio.write_with_channel_selection(|_| channel);
549        });
550    }
551}