redux_rs/middlewares/
thunk.rs

1use crate::{MiddleWare, StoreApi};
2use async_trait::async_trait;
3use std::future::Future;
4use std::sync::Arc;
5
6/// # Thunk middleware
7/// Thunk middleware enables us to introduce side-effects in a redux application.
8///
9/// With this middleware you can dispatch actions and thunks to your store.
10///
11/// ## Fn example
12/// ```
13/// use async_trait::async_trait;
14/// use std::sync::Arc;
15/// use std::time::Duration;
16/// use redux_rs::{Store, StoreApi};
17/// use redux_rs::middlewares::thunk::{ActionOrThunk, ThunkMiddleware, Thunk};
18/// use tokio::time::sleep;
19///
20/// #[derive(Default, Debug, PartialEq)]
21/// struct UserState {
22///     users: Vec<User>,
23/// }
24///
25/// #[derive(Clone, Debug, PartialEq)]
26/// struct User {
27///     id: u8,
28///     name: String,
29/// }
30///
31/// enum UserAction {
32///     UsersLoaded { users: Vec<User> },
33/// }
34///
35/// fn user_reducer(state: UserState, action: UserAction) -> UserState {
36///     match action {
37///         UserAction::UsersLoaded { users } => UserState { users },
38///     }
39/// }
40///
41/// async fn load_users(store_api: Arc<impl StoreApi<UserState, UserAction>>) {
42///     // Emulate api call by delaying for 100 ms
43///     sleep(Duration::from_millis(100)).await;
44///
45///     // Return the data to the store
46///     store_api
47///         .dispatch(UserAction::UsersLoaded {
48///             users: vec![
49///                 User {
50///                     id: 0,
51///                     name: "John Doe".to_string(),
52///                 },
53///                 User {
54///                     id: 1,
55///                     name: "Jane Doe".to_string(),
56///                 },
57///             ],
58///         })
59///         .await;
60/// }
61/// # async fn async_test() {
62/// let store = Store::new(user_reducer).wrap(ThunkMiddleware).await;
63/// store.dispatch(ActionOrThunk::Thunk(Box::new(load_users))).await;
64///
65/// let users = store.select(|state: &UserState| state.users.clone()).await;
66/// assert_eq!(users, vec![]);
67///
68/// sleep(Duration::from_millis(200)).await;
69///
70/// let users = store.select(|state: &UserState| state.users.clone()).await;
71/// assert_eq!(
72///     users,
73///     vec![
74///         User {
75///             id: 0,
76///             name: "John Doe".to_string(),
77///         },
78///         User {
79///             id: 1,
80///             name: "Jane Doe".to_string(),
81///         },
82///     ]
83/// );
84/// # }
85/// ```
86///
87/// ## Trait example
88/// ```
89/// use async_trait::async_trait;
90/// use std::sync::Arc;
91/// use std::time::Duration;
92/// use redux_rs::{Store, StoreApi};
93/// use redux_rs::middlewares::thunk::{ActionOrThunk, ThunkMiddleware, Thunk};
94/// use tokio::time::sleep;
95///
96/// #[derive(Default, Debug, PartialEq)]
97/// struct UserState {
98///     users: Vec<User>,
99/// }
100///
101/// #[derive(Clone, Debug, PartialEq)]
102/// struct User {
103///     id: u8,
104///     name: String,
105/// }
106///
107/// enum UserAction {
108///     UsersLoaded { users: Vec<User> },
109/// }
110///
111/// fn user_reducer(state: UserState, action: UserAction) -> UserState {
112///     match action {
113///         UserAction::UsersLoaded { users } => UserState { users },
114///     }
115/// }
116///
117/// struct LoadUsersThunk;
118/// #[async_trait]
119/// impl<Api> Thunk<UserState, UserAction, Api> for LoadUsersThunk
120///     where
121///         Api: StoreApi<UserState, UserAction> + Send + Sync + 'static,
122/// {
123///     async fn execute(&self, store_api: Arc<Api>) {
124///         // Emulate api call by delaying for 100 ms
125///         sleep(Duration::from_millis(100)).await;
126///
127///         // Return the data to the store
128///         store_api
129///             .dispatch(UserAction::UsersLoaded {
130///                 users: vec![
131///                     User {
132///                         id: 0,
133///                         name: "John Doe".to_string(),
134///                     },
135///                     User {
136///                         id: 1,
137///                         name: "Jane Doe".to_string(),
138///                     },
139///                 ],
140///             })
141///             .await;
142///     }
143/// }
144/// # async fn async_test() {
145/// let store = Store::new(user_reducer).wrap(ThunkMiddleware).await;
146/// store.dispatch(ActionOrThunk::Thunk(Box::new(LoadUsersThunk))).await;
147///
148/// let users = store.select(|state: &UserState| state.users.clone()).await;
149/// assert_eq!(users, vec![]);
150///
151/// sleep(Duration::from_millis(200)).await;
152///
153/// let users = store.select(|state: &UserState| state.users.clone()).await;
154/// assert_eq!(
155///     users,
156///     vec![
157///         User {
158///             id: 0,
159///             name: "John Doe".to_string(),
160///         },
161///         User {
162///             id: 1,
163///             name: "Jane Doe".to_string(),
164///         },
165///     ]
166/// );
167/// # }
168/// ```
169pub struct ThunkMiddleware;
170
171#[async_trait]
172impl<State, Action, Inner> MiddleWare<State, ActionOrThunk<State, Action, Inner>, Inner, Action> for ThunkMiddleware
173where
174    Action: Send + 'static,
175    State: Send + 'static,
176    Inner: StoreApi<State, Action> + Send + Sync + 'static,
177{
178    async fn dispatch(&self, action: ActionOrThunk<State, Action, Inner>, inner: &Arc<Inner>) {
179        match action {
180            ActionOrThunk::Action(action) => {
181                inner.dispatch(action).await;
182            }
183            ActionOrThunk::Thunk(thunk) => {
184                let api = inner.to_owned();
185
186                tokio::spawn(async move {
187                    thunk.execute(api).await;
188                });
189            }
190        }
191    }
192}
193
194pub enum ActionOrThunk<State, Action, Api>
195where
196    Action: Send + 'static,
197    State: Send + 'static,
198    Api: StoreApi<State, Action> + Send + Sync,
199{
200    Action(Action),
201    Thunk(Box<dyn Thunk<State, Action, Api> + Send + Sync>),
202}
203
204#[async_trait]
205pub trait Thunk<State, Action, Api>
206where
207    Action: Send + 'static,
208    State: Send + 'static,
209    Api: StoreApi<State, Action> + Send + Sync + 'static,
210{
211    async fn execute(&self, store_api: Arc<Api>);
212}
213
214#[async_trait]
215impl<F, Fut, State, Action, Api> Thunk<State, Action, Api> for F
216where
217    F: Fn(Arc<Api>) -> Fut + Sync,
218    Fut: Future<Output = ()> + Send,
219    Action: Send + 'static,
220    State: Send + 'static,
221    Api: StoreApi<State, Action> + Send + Sync + 'static,
222{
223    async fn execute(&self, store_api: Arc<Api>) {
224        self(store_api).await;
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::Store;
232    use std::time::Duration;
233    use tokio::time::sleep;
234
235    #[derive(Default, Debug, PartialEq)]
236    struct UserState {
237        users: Vec<User>,
238    }
239
240    #[derive(Clone, Debug, PartialEq)]
241    struct User {
242        id: u8,
243        name: String,
244    }
245
246    enum UserAction {
247        UsersLoaded { users: Vec<User> },
248    }
249
250    fn user_reducer(state: UserState, action: UserAction) -> UserState {
251        match action {
252            UserAction::UsersLoaded { users } => UserState { users },
253        }
254    }
255
256    struct LoadUsersThunk;
257    #[async_trait]
258    impl<Api> Thunk<UserState, UserAction, Api> for LoadUsersThunk
259    where
260        Api: StoreApi<UserState, UserAction> + Send + Sync + 'static,
261    {
262        async fn execute(&self, store_api: Arc<Api>) {
263            // Emulate api call by delaying for 100 ms
264            sleep(Duration::from_millis(100)).await;
265
266            // Return the data to the store
267            store_api
268                .dispatch(UserAction::UsersLoaded {
269                    users: vec![
270                        User {
271                            id: 0,
272                            name: "John Doe".to_string(),
273                        },
274                        User {
275                            id: 1,
276                            name: "Jane Doe".to_string(),
277                        },
278                    ],
279                })
280                .await;
281        }
282    }
283
284    #[tokio::test]
285    async fn load_users_thunk() {
286        let store = Store::new(user_reducer).wrap(ThunkMiddleware).await;
287        store.dispatch(ActionOrThunk::Thunk(Box::new(LoadUsersThunk))).await;
288
289        let users = store.select(|state: &UserState| state.users.clone()).await;
290        assert_eq!(users, vec![]);
291
292        sleep(Duration::from_millis(200)).await;
293
294        let users = store.select(|state: &UserState| state.users.clone()).await;
295        assert_eq!(
296            users,
297            vec![
298                User {
299                    id: 0,
300                    name: "John Doe".to_string(),
301                },
302                User {
303                    id: 1,
304                    name: "Jane Doe".to_string(),
305                },
306            ]
307        );
308    }
309
310    #[tokio::test]
311    async fn load_users_fn_thunk() {
312        let store = Store::new(user_reducer).wrap(ThunkMiddleware).await;
313
314        async fn load_users(store_api: Arc<impl StoreApi<UserState, UserAction>>) {
315            // Emulate api call by delaying for 100 ms
316            sleep(Duration::from_millis(100)).await;
317
318            // Return the data to the store
319            store_api
320                .dispatch(UserAction::UsersLoaded {
321                    users: vec![
322                        User {
323                            id: 0,
324                            name: "John Doe".to_string(),
325                        },
326                        User {
327                            id: 1,
328                            name: "Jane Doe".to_string(),
329                        },
330                    ],
331                })
332                .await;
333        }
334
335        store.dispatch(ActionOrThunk::Thunk(Box::new(load_users))).await;
336
337        let users = store.select(|state: &UserState| state.users.clone()).await;
338        assert_eq!(users, vec![]);
339
340        sleep(Duration::from_millis(200)).await;
341
342        let users = store.select(|state: &UserState| state.users.clone()).await;
343        assert_eq!(
344            users,
345            vec![
346                User {
347                    id: 0,
348                    name: "John Doe".to_string(),
349                },
350                User {
351                    id: 1,
352                    name: "Jane Doe".to_string(),
353                },
354            ]
355        );
356    }
357}