Skip to main content

guinea_core/
binding.rs

1//! Reading a reducer's state without being a UI hook.
2//!
3//! A hook is one way to consume state, not the only one: a reconciler wants a
4//! snapshot plus a subscription scoped to the component instance, an immediate
5//! mode backend polls once a frame, and a retained-property backend wants a
6//! synchronous first call followed by pushes. [`ReducerBinding`] is the
7//! primitive all three are built on.
8
9use std::cell::Ref;
10use std::rc::Rc;
11
12use crate::feature::Dispatch;
13use crate::scope::{DropGuard, Reducer, Scope, Slot, Subscription};
14
15/// A handle to one reducer's state and actions inside one scope.
16///
17/// Names the scope and holds the state cell: reading keeps working after the
18/// scope is removed (the last frame of a page being navigated away from still
19/// renders), while pushing and subscribing quietly stop.
20pub struct ReducerBinding<R: Reducer> {
21    owner: Scope,
22    state: Rc<Slot<R>>,
23    dispatch: Dispatch,
24}
25
26impl<R: Reducer> Clone for ReducerBinding<R> {
27    fn clone(&self) -> Self {
28        Self {
29            owner: self.owner,
30            state: self.state.clone(),
31            dispatch: self.dispatch.clone(),
32        }
33    }
34}
35
36impl<R: Reducer> ReducerBinding<R> {
37    pub(crate) fn new(scope: Scope) -> Self {
38        Self {
39            owner: scope,
40            state: scope.state::<R>(),
41            dispatch: Dispatch::owning::<R>(scope),
42        }
43    }
44
45    /// Borrows the state in place, for as long as nothing changes it.
46    pub fn peek(&self) -> Ref<'_, R> {
47        Ref::map(self.state.borrow(), |state| &**state)
48    }
49
50    /// The state as it is now, shared rather than copied. A change made while
51    /// it is held goes to a copy, so what was read stays as it was.
52    pub fn get(&self) -> Rc<R> {
53        self.state.borrow().clone()
54    }
55
56    /// What may be asked of the actors in the scope that owns this reducer.
57    pub fn dispatch(&self) -> Dispatch {
58        self.dispatch.clone()
59    }
60
61    /// Applies an update directly, without an actor in between. A no-op once
62    /// the owning scope is gone.
63    pub fn push(&self, update: R::Update) {
64        self.owner.push::<R>(update);
65    }
66
67    /// Calls `f` after every change, until the returned [`Subscription`] is
68    /// dropped.
69    ///
70    /// The caller owns the lifetime, which is what a reconciler needs: the
71    /// subscription must end with the component instance, not with the scope,
72    /// or unmounted components accumulate as live listeners.
73    pub fn on_change(&self, f: impl Fn(&R) + 'static) -> Subscription {
74        let state = self.state.clone();
75        self.owner.subscribe::<R>(move || {
76            let now = state.borrow().clone();
77            f(&now)
78        })
79    }
80
81    /// Like [`Self::on_change`], but the subscription lives as long as the
82    /// scope - for backends with no effect system to hang a cleanup on.
83    pub fn on_change_owned(&self, f: impl Fn(&R) + 'static) {
84        let subscription = self.on_change(f);
85        self.owner.own(DropGuard(subscription));
86    }
87
88    /// Calls `f` immediately with the current state, then after every change.
89    pub fn bind(&self, f: impl Fn(&R) + 'static) -> Subscription {
90        f(&self.get());
91        self.on_change(f)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::scope::ScopeTree;
99    use std::cell::Cell;
100
101    #[derive(Clone, Default, Debug)]
102    struct Counter(u32);
103
104    impl Reducer for Counter {
105        type Update = u32;
106
107        fn reduce(&mut self, by: u32) {
108            self.0 += by;
109        }
110    }
111
112    #[test]
113    fn a_binding_reads_on_after_its_scope_is_removed() {
114        let scope = ScopeTree::new();
115        let binding = scope.binding::<Counter>();
116        binding.push(1);
117
118        binding.on_change_owned(|_| {});
119        scope.remove();
120
121        assert_eq!(binding.peek().0, 1, "the state cell outlives the scope");
122        binding.push(1);
123        assert_eq!(binding.peek().0, 1, "pushing into a removed scope is a no-op");
124    }
125
126    #[test]
127    fn subscribing_to_a_removed_scope_hears_nothing() {
128        let scope = ScopeTree::new();
129        let binding = scope.binding::<Counter>();
130        scope.remove();
131
132        let seen = Rc::new(Cell::new(false));
133        let recorder = seen.clone();
134        let _subscription = binding.on_change(move |_| recorder.set(true));
135        binding.push(1);
136
137        assert!(!seen.get());
138    }
139
140    #[test]
141    fn dropping_a_subscription_after_its_scope_is_harmless() {
142        let scope = ScopeTree::new();
143        let subscription = scope.binding::<Counter>().on_change(|_| {});
144        scope.remove();
145
146        drop(subscription);
147    }
148
149    #[test]
150    fn on_change_sees_every_push_until_dropped() {
151        let scope = ScopeTree::new();
152        let binding = scope.binding::<Counter>();
153
154        let seen = Rc::new(Cell::new(0u32));
155        let recorder = seen.clone();
156        let subscription = binding.on_change(move |counter| recorder.set(counter.0));
157
158        binding.push(2);
159        binding.push(3);
160        assert_eq!(seen.get(), 5);
161
162        drop(subscription);
163        binding.push(4);
164        assert_eq!(seen.get(), 5, "no delivery after the subscription is dropped");
165    }
166
167    #[test]
168    fn bind_calls_back_before_anything_changes() {
169        let scope = ScopeTree::new();
170        let binding = scope.binding::<Counter>();
171        binding.push(7);
172
173        let seen = Rc::new(Cell::new(None));
174        let recorder = seen.clone();
175        let _subscription = binding.bind(move |counter| recorder.set(Some(counter.0)));
176
177        assert_eq!(seen.get(), Some(7));
178    }
179
180    #[test]
181    fn a_read_is_shared_and_a_change_while_it_is_held_goes_to_a_copy() {
182        let scope = ScopeTree::new();
183        let binding = scope.binding::<Counter>();
184        binding.push(1);
185
186        let first = binding.get();
187        let again = binding.get();
188        assert!(Rc::ptr_eq(&first, &again), "reading twice copies nothing");
189
190        binding.push(2);
191        assert_eq!(first.0, 1, "what was read stays as it was");
192        assert_eq!(binding.get().0, 3);
193
194        drop((first, again));
195        let before = Rc::as_ptr(&binding.get());
196        binding.push(4);
197        assert_eq!(Rc::as_ptr(&binding.get()), before, "nobody held it, so no copy");
198    }
199}