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 crate::functional::{hook, Hook, HookContext};
8use crate::html::IntoPropValue;
9use crate::Callback;
10
11type DispatchFn<T> = Rc<dyn Fn(<T as Reducible>::Action)>;
12
13/// A trait that implements a reducer function of a type.
14pub trait Reducible {
15 /// The action type of the reducer.
16 type Action;
17
18 /// The reducer function.
19 fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self>;
20}
21
22struct UseReducer<T>
23where
24 T: Reducible,
25{
26 current_state: Rc<RefCell<Rc<T>>>,
27
28 dispatch: DispatchFn<T>,
29}
30
31/// State handle for [`use_reducer`] and [`use_reducer_eq`] hook
32pub struct UseReducerHandle<T>
33where
34 T: Reducible,
35{
36 value: Rc<T>,
37 dispatch: DispatchFn<T>,
38}
39
40impl<T> UseReducerHandle<T>
41where
42 T: Reducible,
43{
44 /// Dispatch the given action to the reducer.
45 pub fn dispatch(&self, value: T::Action) {
46 (self.dispatch)(value)
47 }
48
49 /// Returns the dispatcher of the current state.
50 pub fn dispatcher(&self) -> UseReducerDispatcher<T> {
51 UseReducerDispatcher {
52 dispatch: self.dispatch.clone(),
53 }
54 }
55}
56
57impl<T> Deref for UseReducerHandle<T>
58where
59 T: Reducible,
60{
61 type Target = T;
62
63 fn deref(&self) -> &Self::Target {
64 &self.value
65 }
66}
67
68impl<T> Clone for UseReducerHandle<T>
69where
70 T: Reducible,
71{
72 fn clone(&self) -> Self {
73 Self {
74 value: Rc::clone(&self.value),
75 dispatch: Rc::clone(&self.dispatch),
76 }
77 }
78}
79
80impl<T> fmt::Debug for UseReducerHandle<T>
81where
82 T: Reducible + fmt::Debug,
83{
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.debug_struct("UseReducerHandle")
86 .field("value", &format!("{:?}", self.value))
87 .finish()
88 }
89}
90
91impl<T> PartialEq for UseReducerHandle<T>
92where
93 T: Reducible + PartialEq,
94{
95 fn eq(&self, rhs: &Self) -> bool {
96 self.value == rhs.value
97 }
98}
99
100/// Dispatcher handle for [`use_reducer`] and [`use_reducer_eq`] hook
101pub struct UseReducerDispatcher<T>
102where
103 T: Reducible,
104{
105 dispatch: DispatchFn<T>,
106}
107
108impl<T> Clone for UseReducerDispatcher<T>
109where
110 T: Reducible,
111{
112 fn clone(&self) -> Self {
113 Self {
114 dispatch: Rc::clone(&self.dispatch),
115 }
116 }
117}
118
119impl<T> fmt::Debug for UseReducerDispatcher<T>
120where
121 T: Reducible + fmt::Debug,
122{
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 f.debug_struct("UseReducerDispatcher").finish()
125 }
126}
127
128impl<T> PartialEq for UseReducerDispatcher<T>
129where
130 T: Reducible,
131{
132 fn eq(&self, rhs: &Self) -> bool {
133 // We are okay with comparisons from different compilation units to result in false
134 // not-equal results. This should only lead in the worst-case to some unneeded
135 // re-renders.
136 #[allow(ambiguous_wide_pointer_comparisons)]
137 Rc::ptr_eq(&self.dispatch, &rhs.dispatch)
138 }
139}
140
141impl<T> From<UseReducerDispatcher<T>> for Callback<<T as Reducible>::Action>
142where
143 T: Reducible,
144{
145 fn from(val: UseReducerDispatcher<T>) -> Self {
146 Callback { cb: val.dispatch }
147 }
148}
149
150impl<T> IntoPropValue<Callback<<T as Reducible>::Action>> for UseReducerDispatcher<T>
151where
152 T: Reducible,
153{
154 fn into_prop_value(self) -> Callback<<T as Reducible>::Action> {
155 Callback { cb: self.dispatch }
156 }
157}
158
159impl<T> UseReducerDispatcher<T>
160where
161 T: Reducible,
162{
163 /// Dispatch the given action to the reducer.
164 pub fn dispatch(&self, value: T::Action) {
165 (self.dispatch)(value)
166 }
167
168 /// Get a callback, invoking which is equivalent to calling `dispatch()`
169 /// on this same dispatcher.
170 pub fn to_callback(&self) -> Callback<<T as Reducible>::Action> {
171 Callback {
172 cb: self.dispatch.clone(),
173 }
174 }
175}
176
177/// The base function of [`use_reducer`] and [`use_reducer_eq`]
178fn use_reducer_base<'hook, T>(
179 init_fn: impl 'hook + FnOnce() -> T,
180 should_render_fn: fn(&T, &T) -> bool,
181) -> impl 'hook + Hook<Output = UseReducerHandle<T>>
182where
183 T: Reducible + 'static,
184{
185 struct HookProvider<'hook, T, F>
186 where
187 T: Reducible + 'static,
188 F: 'hook + FnOnce() -> T,
189 {
190 _marker: PhantomData<&'hook ()>,
191
192 init_fn: F,
193 should_render_fn: fn(&T, &T) -> bool,
194 }
195
196 impl<'hook, T, F> Hook for HookProvider<'hook, T, F>
197 where
198 T: Reducible + 'static,
199 F: 'hook + FnOnce() -> T,
200 {
201 type Output = UseReducerHandle<T>;
202
203 fn run(self, ctx: &mut HookContext) -> Self::Output {
204 let Self {
205 init_fn,
206 should_render_fn,
207 ..
208 } = self;
209
210 let state = ctx.next_state(move |re_render| {
211 let val = Rc::new(RefCell::new(Rc::new(init_fn())));
212 let should_render_fn = Rc::new(should_render_fn);
213
214 UseReducer {
215 current_state: val.clone(),
216 dispatch: Rc::new(move |action: T::Action| {
217 let should_render = {
218 let should_render_fn = should_render_fn.clone();
219 let mut val = val.borrow_mut();
220 let next_val = (*val).clone().reduce(action);
221 let should_render = should_render_fn(&next_val, &val);
222 *val = next_val;
223
224 should_render
225 };
226
227 // Currently, this triggers a render immediately, so we need to release the
228 // borrowed reference first.
229 if should_render {
230 re_render()
231 }
232 }),
233 }
234 });
235
236 let value = state.current_state.borrow().clone();
237 let dispatch = state.dispatch.clone();
238
239 UseReducerHandle { value, dispatch }
240 }
241 }
242
243 HookProvider {
244 _marker: PhantomData,
245 init_fn,
246 should_render_fn,
247 }
248}
249
250/// This hook is an alternative to [`use_state`](super::use_state()).
251/// It is used to handle component's state and is used when complex actions needs to be performed on
252/// said state.
253///
254/// The state is expected to implement the [`Reducible`] trait which provides an `Action` type and a
255/// reducer function.
256///
257/// The state object returned by the initial state function is required to
258/// implement a `Reducible` trait which defines the associated `Action` type and a
259/// reducer function.
260///
261/// This hook will always trigger a re-render upon receiving an action. See
262/// [`use_reducer_eq`] if you want the component to only re-render when the state changes.
263///
264/// # Example
265/// ```rust
266/// # use yew::prelude::*;
267/// # use std::rc::Rc;
268/// #
269///
270/// /// reducer's Action
271/// enum CounterAction {
272/// Double,
273/// Square,
274/// }
275///
276/// /// reducer's State
277/// struct CounterState {
278/// counter: i32,
279/// }
280///
281/// impl Default for CounterState {
282/// fn default() -> Self {
283/// Self { counter: 1 }
284/// }
285/// }
286///
287/// impl Reducible for CounterState {
288/// /// Reducer Action Type
289/// type Action = CounterAction;
290///
291/// /// Reducer Function
292/// fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
293/// let next_ctr = match action {
294/// CounterAction::Double => self.counter * 2,
295/// CounterAction::Square => self.counter.pow(2),
296/// };
297///
298/// Self { counter: next_ctr }.into()
299/// }
300/// }
301///
302/// #[function_component(UseReducer)]
303/// fn reducer() -> Html {
304/// // The use_reducer hook takes an initialization function which will be called only once.
305/// let counter = use_reducer(CounterState::default);
306///
307/// let double_onclick = {
308/// let counter = counter.clone();
309/// Callback::from(move |_| counter.dispatch(CounterAction::Double))
310/// };
311/// let square_onclick = {
312/// let counter = counter.clone();
313/// Callback::from(move |_| counter.dispatch(CounterAction::Square))
314/// };
315///
316/// html! {
317/// <>
318/// <div id="result">{ counter.counter }</div>
319///
320/// <button onclick={double_onclick}>{ "Double" }</button>
321/// <button onclick={square_onclick}>{ "Square" }</button>
322/// </>
323/// }
324/// }
325/// ```
326///
327/// # Tip
328///
329/// The dispatch function is guaranteed to be the same across the entire
330/// component lifecycle. You can safely omit the `UseReducerHandle` from the
331/// dependents of `use_effect_with` if you only intend to dispatch
332/// values from within the hooks.
333///
334/// # Caution
335///
336/// The value held in the handle will reflect the value of at the time the
337/// handle is returned by the `use_reducer`. It is possible that the handle does
338/// not dereference to an up to date value if you are moving it into a
339/// `use_effect_with` hook. You can register the
340/// state to the dependents so the hook can be updated when the value changes.
341#[hook]
342pub fn use_reducer<T, F>(init_fn: F) -> UseReducerHandle<T>
343where
344 T: Reducible + 'static,
345 F: FnOnce() -> T,
346{
347 use_reducer_base(init_fn, |_, _| true)
348}
349
350/// [`use_reducer`] but only re-renders when `prev_state != next_state`.
351///
352/// This requires the state to implement [`PartialEq`] in addition to the [`Reducible`] trait
353/// required by [`use_reducer`].
354#[hook]
355pub fn use_reducer_eq<T, F>(init_fn: F) -> UseReducerHandle<T>
356where
357 T: Reducible + PartialEq + 'static,
358 F: FnOnce() -> T,
359{
360 use_reducer_base(init_fn, T::ne)
361}