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}