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 **self == **rhs
149 }
150}
151
152impl<T> ImplicitClone for UseReducerHandle<T> where T: Reducible {}
153
154/// Dispatcher handle for [`use_reducer`] and [`use_reducer_eq`] hook
155pub struct UseReducerDispatcher<T>
156where
157 T: Reducible,
158{
159 dispatch: DispatchFn<T>,
160}
161
162impl<T> Clone for UseReducerDispatcher<T>
163where
164 T: Reducible,
165{
166 fn clone(&self) -> Self {
167 Self {
168 dispatch: Rc::clone(&self.dispatch),
169 }
170 }
171}
172
173impl<T> fmt::Debug for UseReducerDispatcher<T>
174where
175 T: Reducible + fmt::Debug,
176{
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.debug_struct("UseReducerDispatcher").finish()
179 }
180}
181
182impl<T> PartialEq for UseReducerDispatcher<T>
183where
184 T: Reducible,
185{
186 fn eq(&self, rhs: &Self) -> bool {
187 // We are okay with comparisons from different compilation units to result in false
188 // not-equal results. This should only lead in the worst-case to some unneeded
189 // re-renders.
190 #[allow(ambiguous_wide_pointer_comparisons)]
191 Rc::ptr_eq(&self.dispatch, &rhs.dispatch)
192 }
193}
194
195impl<T> ImplicitClone for UseReducerDispatcher<T> where T: Reducible {}
196
197impl<T> From<UseReducerDispatcher<T>> for Callback<<T as Reducible>::Action>
198where
199 T: Reducible,
200{
201 fn from(val: UseReducerDispatcher<T>) -> Self {
202 Callback { cb: val.dispatch }
203 }
204}
205
206impl<T> IntoPropValue<Callback<<T as Reducible>::Action>> for UseReducerDispatcher<T>
207where
208 T: Reducible,
209{
210 fn into_prop_value(self) -> Callback<<T as Reducible>::Action> {
211 Callback { cb: self.dispatch }
212 }
213}
214
215impl<T> UseReducerDispatcher<T>
216where
217 T: Reducible,
218{
219 /// Dispatch the given action to the reducer.
220 pub fn dispatch(&self, value: T::Action) {
221 (self.dispatch)(value)
222 }
223
224 /// Get a callback, invoking which is equivalent to calling `dispatch()`
225 /// on this same dispatcher.
226 pub fn to_callback(&self) -> Callback<<T as Reducible>::Action> {
227 Callback {
228 cb: self.dispatch.clone(),
229 }
230 }
231}
232
233/// The base function of [`use_reducer`] and [`use_reducer_eq`]
234fn use_reducer_base<'hook, T>(
235 init_fn: impl 'hook + FnOnce() -> T,
236 should_render_fn: fn(&T, &T) -> bool,
237) -> impl 'hook + Hook<Output = UseReducerHandle<T>>
238where
239 T: Reducible + 'static,
240{
241 struct HookProvider<'hook, T, F>
242 where
243 T: Reducible + 'static,
244 F: 'hook + FnOnce() -> T,
245 {
246 _marker: PhantomData<&'hook ()>,
247
248 init_fn: F,
249 should_render_fn: fn(&T, &T) -> bool,
250 }
251
252 impl<'hook, T, F> Hook for HookProvider<'hook, T, F>
253 where
254 T: Reducible + 'static,
255 F: 'hook + FnOnce() -> T,
256 {
257 type Output = UseReducerHandle<T>;
258
259 fn run(self, ctx: &mut HookContext) -> Self::Output {
260 let Self {
261 init_fn,
262 should_render_fn,
263 ..
264 } = self;
265
266 let state = ctx.next_state(move |re_render| {
267 let val = Rc::new(RefCell::new(Rc::new(init_fn())));
268 let should_render_fn = Rc::new(should_render_fn);
269
270 UseReducer {
271 current_state: val.clone(),
272 dispatch: Rc::new(move |action: T::Action| {
273 let should_render = {
274 let should_render_fn = should_render_fn.clone();
275 let mut val = val.borrow_mut();
276 let next_val = (*val).clone().reduce(action);
277 let should_render = should_render_fn(&next_val, &val);
278 *val = next_val;
279
280 should_render
281 };
282
283 // Currently, this triggers a render immediately, so we need to release the
284 // borrowed reference first.
285 if should_render {
286 re_render()
287 }
288 }),
289 }
290 });
291
292 let current_state = state.current_state.clone();
293 let snapshot = state.current_state.borrow().clone();
294 let dispatch = state.dispatch.clone();
295
296 UseReducerHandle {
297 current_state,
298 deref_history: RefCell::new(vec![snapshot]),
299 dispatch,
300 }
301 }
302 }
303
304 HookProvider {
305 _marker: PhantomData,
306 init_fn,
307 should_render_fn,
308 }
309}
310
311/// This hook is an alternative to [`use_state`](super::use_state()).
312/// It is used to handle component's state and is used when complex actions needs to be performed on
313/// said state.
314///
315/// The state is expected to implement the [`Reducible`] trait which provides an `Action` type and a
316/// reducer function.
317///
318/// The state object returned by the initial state function is required to
319/// implement a `Reducible` trait which defines the associated `Action` type and a
320/// reducer function.
321///
322/// This hook will trigger a re-render whenever the reducer function produces a new `Rc` value upon
323/// receiving an action. If the reducer function simply returns the original `Rc` then the component
324/// will not re-render. See [`use_reducer_eq`] if you want the component to first compare the old
325/// and new state and only re-render when the state actually changes.
326///
327/// To cause a re-render even if the reducer function returns the same `Rc`, take a look at
328/// [`use_force_update`].
329///
330/// # Example
331/// ```rust
332/// # use yew::prelude::*;
333/// # use std::rc::Rc;
334/// #
335///
336/// /// reducer's Action
337/// enum CounterAction {
338/// Double,
339/// Square,
340/// }
341///
342/// /// reducer's State
343/// struct CounterState {
344/// counter: i32,
345/// }
346///
347/// impl Default for CounterState {
348/// fn default() -> Self {
349/// Self { counter: 1 }
350/// }
351/// }
352///
353/// impl Reducible for CounterState {
354/// /// Reducer Action Type
355/// type Action = CounterAction;
356///
357/// /// Reducer Function
358/// fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
359/// let next_ctr = match action {
360/// CounterAction::Double => self.counter * 2,
361/// CounterAction::Square => self.counter.pow(2),
362/// };
363///
364/// Self { counter: next_ctr }.into()
365/// }
366/// }
367///
368/// #[component(UseReducer)]
369/// fn reducer() -> Html {
370/// // The use_reducer hook takes an initialization function which will be called only once.
371/// let counter = use_reducer(CounterState::default);
372///
373/// let double_onclick = {
374/// let counter = counter.clone();
375/// Callback::from(move |_| counter.dispatch(CounterAction::Double))
376/// };
377/// let square_onclick = {
378/// let counter = counter.clone();
379/// Callback::from(move |_| counter.dispatch(CounterAction::Square))
380/// };
381///
382/// html! {
383/// <>
384/// <div id="result">{ counter.counter }</div>
385///
386/// <button onclick={double_onclick}>{ "Double" }</button>
387/// <button onclick={square_onclick}>{ "Square" }</button>
388/// </>
389/// }
390/// }
391/// ```
392///
393/// # Tip
394///
395/// The dispatch function is guaranteed to be the same across the entire
396/// component lifecycle. You can safely omit the `UseReducerHandle` from the
397/// dependents of `use_effect_with` if you only intend to dispatch
398/// values from within the hooks.
399///
400/// # Caution
401///
402/// The value held in the handle will reflect the value of at the time the
403/// handle is returned by the `use_reducer`. It is possible that the handle does
404/// not dereference to an up to date value if you are moving it into a
405/// `use_effect_with` hook. You can register the
406/// state to the dependents so the hook can be updated when the value changes.
407#[hook]
408pub fn use_reducer<T, F>(init_fn: F) -> UseReducerHandle<T>
409where
410 T: Reducible + 'static,
411 F: FnOnce() -> T,
412{
413 use_reducer_base(init_fn, |a, b| !address_eq(a, b))
414}
415
416/// [`use_reducer`] but only re-renders when `prev_state != next_state`.
417///
418/// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait
419/// required by [`use_reducer`].
420#[hook]
421pub fn use_reducer_eq<T, F>(init_fn: F) -> UseReducerHandle<T>
422where
423 T: Reducible + PartialEq + 'static,
424 F: FnOnce() -> T,
425{
426 use_reducer_base(init_fn, |a, b| !address_eq(a, b) && a != b)
427}
428
429/// Check if two references point to the same address.
430fn address_eq<T>(a: &T, b: &T) -> bool {
431 std::ptr::eq(a as *const T, b as *const T)
432}