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