Skip to main content
This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew/functional/hooks/
use_reducer.rs

1use std::cell::RefCell;
2use std::fmt;
3use std::marker::PhantomData;
4use std::ops::Deref;
5use std::rc::Rc;
6
7use implicit_clone::ImplicitClone;
8
9use crate::functional::{hook, Hook, HookContext};
10use crate::html::IntoPropValue;
11use crate::Callback;
12
13type DispatchFn<T> = Rc<dyn Fn(<T as Reducible>::Action)>;
14
15/// A trait that implements a reducer function of a type.
16pub trait Reducible {
17    /// The action type of the reducer.
18    type Action;
19
20    /// The reducer function.
21    fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self>;
22}
23
24struct UseReducer<T>
25where
26    T: Reducible,
27{
28    current_state: Rc<RefCell<Rc<T>>>,
29
30    dispatch: DispatchFn<T>,
31}
32
33/// State handle for [`use_reducer`] and [`use_reducer_eq`] hook
34pub struct UseReducerHandle<T>
35where
36    T: Reducible,
37{
38    /// Shared source of truth, updated synchronously by dispatch.
39    current_state: Rc<RefCell<Rc<T>>>,
40    /// Accumulates `Rc<T>` clones returned by [`Deref::deref`] so that references
41    /// remain valid for the lifetime of this handle. Reset on each re-render when
42    /// a new handle is created.
43    deref_history: RefCell<Vec<Rc<T>>>,
44    dispatch: DispatchFn<T>,
45}
46
47impl<T> UseReducerHandle<T>
48where
49    T: Reducible,
50{
51    /// Dispatch the given action to the reducer.
52    pub fn dispatch(&self, value: T::Action) {
53        (self.dispatch)(value)
54    }
55
56    /// Returns the dispatcher of the current state.
57    pub fn dispatcher(&self) -> UseReducerDispatcher<T> {
58        UseReducerDispatcher {
59            dispatch: self.dispatch.clone(),
60        }
61    }
62}
63
64impl<T> Deref for UseReducerHandle<T>
65where
66    T: Reducible,
67{
68    type Target = T;
69
70    fn deref(&self) -> &Self::Target {
71        let rc = match self.current_state.try_borrow() {
72            Ok(shared) => Rc::clone(&*shared),
73            Err(_) => {
74                // RefCell is mutably borrowed (during dispatch). Use the last
75                // value we successfully read.
76                let history = self.deref_history.borrow();
77                Rc::clone(history.last().expect("deref_history is never empty"))
78            }
79        };
80
81        let ptr: *const T = Rc::as_ptr(&rc);
82
83        // Only store a new entry when the Rc allocation differs from the most
84        // recent one, avoiding unbounded growth from repeated reads of the same
85        // state.
86        {
87            let mut history = self.deref_history.borrow_mut();
88            if !Rc::ptr_eq(history.last().expect("deref_history is never empty"), &rc) {
89                history.push(rc);
90            }
91        }
92
93        // SAFETY: `ptr` points into the heap allocation of an `Rc<T>`. That Rc
94        // is kept alive in `self.deref_history` (either the entry we just pushed,
95        // or a previous entry with the same allocation). `deref_history` lives as
96        // long as `self`, and `Rc` guarantees its heap allocation stays live while
97        // any clone exists. Therefore `ptr` is valid for the lifetime of `&self`.
98        unsafe { &*ptr }
99    }
100}
101
102impl<T> Clone for UseReducerHandle<T>
103where
104    T: Reducible,
105{
106    fn clone(&self) -> Self {
107        // Take a fresh snapshot so the clone's deref_history is never empty.
108        let snapshot = match self.current_state.try_borrow() {
109            Ok(shared) => Rc::clone(&*shared),
110            Err(_) => {
111                let history = self.deref_history.borrow();
112                Rc::clone(history.last().expect("deref_history is never empty"))
113            }
114        };
115        Self {
116            current_state: Rc::clone(&self.current_state),
117            deref_history: RefCell::new(vec![snapshot]),
118            dispatch: Rc::clone(&self.dispatch),
119        }
120    }
121}
122
123impl<T> fmt::Debug for UseReducerHandle<T>
124where
125    T: Reducible + fmt::Debug,
126{
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        let value = if let Ok(rc_ref) = self.current_state.try_borrow() {
129            format!("{:?}", *rc_ref)
130        } else {
131            let history = self.deref_history.borrow();
132            format!(
133                "{:?}",
134                **history.last().expect("deref_history is never empty")
135            )
136        };
137        f.debug_struct("UseReducerHandle")
138            .field("value", &value)
139            .finish()
140    }
141}
142
143impl<T> PartialEq for UseReducerHandle<T>
144where
145    T: Reducible + PartialEq,
146{
147    fn eq(&self, rhs: &Self) -> bool {
148        let self_snapshot = self.deref_history.borrow();
149        let rhs_snapshot = rhs.deref_history.borrow();
150        *self_snapshot[0] == *rhs_snapshot[0]
151    }
152}
153
154impl<T> ImplicitClone for UseReducerHandle<T> where T: Reducible {}
155
156/// Dispatcher handle for [`use_reducer`] and [`use_reducer_eq`] hook
157pub struct UseReducerDispatcher<T>
158where
159    T: Reducible,
160{
161    dispatch: DispatchFn<T>,
162}
163
164impl<T> Clone for UseReducerDispatcher<T>
165where
166    T: Reducible,
167{
168    fn clone(&self) -> Self {
169        Self {
170            dispatch: Rc::clone(&self.dispatch),
171        }
172    }
173}
174
175impl<T> fmt::Debug for UseReducerDispatcher<T>
176where
177    T: Reducible + fmt::Debug,
178{
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        f.debug_struct("UseReducerDispatcher").finish()
181    }
182}
183
184impl<T> PartialEq for UseReducerDispatcher<T>
185where
186    T: Reducible,
187{
188    fn eq(&self, rhs: &Self) -> bool {
189        // We are okay with comparisons from different compilation units to result in false
190        // not-equal results. This should only lead in the worst-case to some unneeded
191        // re-renders.
192        #[allow(ambiguous_wide_pointer_comparisons)]
193        Rc::ptr_eq(&self.dispatch, &rhs.dispatch)
194    }
195}
196
197impl<T> ImplicitClone for UseReducerDispatcher<T> where T: Reducible {}
198
199impl<T> From<UseReducerDispatcher<T>> for Callback<<T as Reducible>::Action>
200where
201    T: Reducible,
202{
203    fn from(val: UseReducerDispatcher<T>) -> Self {
204        Callback { cb: val.dispatch }
205    }
206}
207
208impl<T> IntoPropValue<Callback<<T as Reducible>::Action>> for UseReducerDispatcher<T>
209where
210    T: Reducible,
211{
212    fn into_prop_value(self) -> Callback<<T as Reducible>::Action> {
213        Callback { cb: self.dispatch }
214    }
215}
216
217impl<T> UseReducerDispatcher<T>
218where
219    T: Reducible,
220{
221    /// Dispatch the given action to the reducer.
222    pub fn dispatch(&self, value: T::Action) {
223        (self.dispatch)(value)
224    }
225
226    /// Get a callback, invoking which is equivalent to calling `dispatch()`
227    /// on this same dispatcher.
228    pub fn to_callback(&self) -> Callback<<T as Reducible>::Action> {
229        Callback {
230            cb: self.dispatch.clone(),
231        }
232    }
233}
234
235/// The base function of [`use_reducer`] and [`use_reducer_eq`]
236fn use_reducer_base<'hook, T>(
237    init_fn: impl 'hook + FnOnce() -> T,
238    should_render_fn: fn(&T, &T) -> bool,
239) -> impl 'hook + Hook<Output = UseReducerHandle<T>>
240where
241    T: Reducible + 'static,
242{
243    struct HookProvider<'hook, T, F>
244    where
245        T: Reducible + 'static,
246        F: 'hook + FnOnce() -> T,
247    {
248        _marker: PhantomData<&'hook ()>,
249
250        init_fn: F,
251        should_render_fn: fn(&T, &T) -> bool,
252    }
253
254    impl<'hook, T, F> Hook for HookProvider<'hook, T, F>
255    where
256        T: Reducible + 'static,
257        F: 'hook + FnOnce() -> T,
258    {
259        type Output = UseReducerHandle<T>;
260
261        fn run(self, ctx: &mut HookContext) -> Self::Output {
262            let Self {
263                init_fn,
264                should_render_fn,
265                ..
266            } = self;
267
268            let state = ctx.next_state(move |re_render| {
269                let val = Rc::new(RefCell::new(Rc::new(init_fn())));
270                let should_render_fn = Rc::new(should_render_fn);
271
272                UseReducer {
273                    current_state: val.clone(),
274                    dispatch: Rc::new(move |action: T::Action| {
275                        let should_render = {
276                            let should_render_fn = should_render_fn.clone();
277                            let mut val = val.borrow_mut();
278                            let next_val = (*val).clone().reduce(action);
279                            let should_render = should_render_fn(&next_val, &val);
280                            *val = next_val;
281
282                            should_render
283                        };
284
285                        // Currently, this triggers a render immediately, so we need to release the
286                        // borrowed reference first.
287                        if should_render {
288                            re_render()
289                        }
290                    }),
291                }
292            });
293
294            let current_state = state.current_state.clone();
295            let snapshot = state.current_state.borrow().clone();
296            let dispatch = state.dispatch.clone();
297
298            UseReducerHandle {
299                current_state,
300                deref_history: RefCell::new(vec![snapshot]),
301                dispatch,
302            }
303        }
304    }
305
306    HookProvider {
307        _marker: PhantomData,
308        init_fn,
309        should_render_fn,
310    }
311}
312
313/// This hook is an alternative to [`use_state`](super::use_state()).
314/// It is used to handle component's state and is used when complex actions needs to be performed on
315/// said state.
316///
317/// The state is expected to implement the [`Reducible`] trait which provides an `Action` type and a
318/// reducer function.
319///
320/// The state object returned by the initial state function is required to
321/// implement a `Reducible` trait which defines the associated `Action` type and a
322/// reducer function.
323///
324/// This hook will trigger a re-render whenever the reducer function produces a new `Rc` value upon
325/// receiving an action. If the reducer function simply returns the original `Rc` then the component
326/// will not re-render. See [`use_reducer_eq`] if you want the component to first compare the old
327/// and new state and only re-render when the state actually changes.
328///
329/// To cause a re-render even if the reducer function returns the same `Rc`, take a look at
330/// [`use_force_update`].
331///
332/// # Example
333/// ```rust
334/// # use yew::prelude::*;
335/// # use std::rc::Rc;
336/// #
337///
338/// /// reducer's Action
339/// enum CounterAction {
340///     Double,
341///     Square,
342/// }
343///
344/// /// reducer's State
345/// struct CounterState {
346///     counter: i32,
347/// }
348///
349/// impl Default for CounterState {
350///     fn default() -> Self {
351///         Self { counter: 1 }
352///     }
353/// }
354///
355/// impl Reducible for CounterState {
356///     /// Reducer Action Type
357///     type Action = CounterAction;
358///
359///     /// Reducer Function
360///     fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
361///         let next_ctr = match action {
362///             CounterAction::Double => self.counter * 2,
363///             CounterAction::Square => self.counter.pow(2),
364///         };
365///
366///         Self { counter: next_ctr }.into()
367///     }
368/// }
369///
370/// #[component(UseReducer)]
371/// fn reducer() -> Html {
372///     // The use_reducer hook takes an initialization function which will be called only once.
373///     let counter = use_reducer(CounterState::default);
374///
375///     let double_onclick = {
376///         let counter = counter.clone();
377///         Callback::from(move |_| counter.dispatch(CounterAction::Double))
378///     };
379///     let square_onclick = {
380///         let counter = counter.clone();
381///         Callback::from(move |_| counter.dispatch(CounterAction::Square))
382///     };
383///
384///     html! {
385///         <>
386///             <div id="result">{ counter.counter }</div>
387///
388///             <button onclick={double_onclick}>{ "Double" }</button>
389///             <button onclick={square_onclick}>{ "Square" }</button>
390///         </>
391///     }
392/// }
393/// ```
394///
395/// # Tip
396///
397/// The dispatch function is guaranteed to be the same across the entire
398/// component lifecycle. You can safely omit the `UseReducerHandle` from the
399/// dependents of `use_effect_with` if you only intend to dispatch
400/// values from within the hooks.
401///
402/// # Caution
403///
404/// The value held in the handle will reflect the value of at the time the
405/// handle is returned by the `use_reducer`. It is possible that the handle does
406/// not dereference to an up to date value if you are moving it into a
407/// `use_effect_with` hook. You can register the
408/// state to the dependents so the hook can be updated when the value changes.
409#[hook]
410pub fn use_reducer<T, F>(init_fn: F) -> UseReducerHandle<T>
411where
412    T: Reducible + 'static,
413    F: FnOnce() -> T,
414{
415    use_reducer_base(init_fn, |a, b| !address_eq(a, b))
416}
417
418/// [`use_reducer`] but only re-renders when `prev_state != next_state`.
419///
420/// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait
421/// required by [`use_reducer`].
422#[hook]
423pub fn use_reducer_eq<T, F>(init_fn: F) -> UseReducerHandle<T>
424where
425    T: Reducible + PartialEq + 'static,
426    F: FnOnce() -> T,
427{
428    use_reducer_base(init_fn, |a, b| !address_eq(a, b) && a != b)
429}
430
431/// Check if two references point to the same address.
432fn address_eq<T>(a: &T, b: &T) -> bool {
433    std::ptr::eq(a as *const T, b as *const T)
434}