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}