├── .gitignore ├── screenshots └── reactive_state_20200601.png ├── Cargo.toml ├── LICENSE.txt ├── .github └── workflows │ └── rust.yml ├── README.md └── src ├── middleware ├── simple_logger.rs ├── mod.rs └── web_logger.rs ├── listener.rs ├── lib.rs ├── provider.rs ├── reducer.rs └── store.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /screenshots/reactive_state_20200601.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kellpossible/reactive-state/HEAD/screenshots/reactive_state_20200601.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reactive-state" 3 | description = "A library for managing global application state (similar to redux)." 4 | version = "0.3.0" 5 | authors = ["Luke Frisken "] 6 | edition = "2018" 7 | license = "MIT" 8 | exclude = ["build-docs.sh", "screenshots/**"] 9 | readme = "README.md" 10 | repository = "https://github.com/kellpossible/reactive-state" 11 | 12 | [badges] 13 | maintenance = { status = "actively-developed" } 14 | 15 | [dependencies] 16 | yew = { version = "0.18", optional = true } 17 | wasm-bindgen = { version = "0.2", optional = true } 18 | js-sys = { version = "0.3", optional = true } 19 | web-sys = { version = "0.3", optional = true } 20 | serde = { version = "1", optional = true } 21 | log = { version = "0.4", optional = true } 22 | 23 | [features] 24 | default = [] 25 | web_logger = ["serde", "wasm-bindgen/serde-serialize", "web-sys", "js-sys"] 26 | simple_logger = ["log"] 27 | 28 | [package.metadata.docs.rs] 29 | all-features = true 30 | rustdoc-args = ["--cfg", "docsrs"] -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 Luke Frisken 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: Test Suite 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | profile: minimal 18 | toolchain: stable 19 | override: true 20 | - uses: actions-rs/cargo@v1 21 | with: 22 | command: test 23 | args: --all-features 24 | 25 | fmt: 26 | name: Rustfmt 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: actions-rs/toolchain@v1 31 | with: 32 | profile: minimal 33 | toolchain: stable 34 | override: true 35 | - run: rustup component add rustfmt 36 | - uses: actions-rs/cargo@v1 37 | with: 38 | command: fmt 39 | args: --all -- --check 40 | 41 | clippy: 42 | name: Clippy 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v1 46 | - uses: actions-rs/toolchain@v1 47 | with: 48 | toolchain: nightly 49 | components: clippy 50 | override: true 51 | # Note that there is no release tag available yet 52 | # and the following code will use master branch HEAD 53 | # all the time. 54 | - uses: actions-rs/clippy@master 55 | with: 56 | args: --all-features -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reactive-state [![crates.io badge](https://img.shields.io/crates/v/reactive-state.svg)](https://crates.io/crates/reactive-state) [![docs.rs badge](https://docs.rs/reactive-state/badge.svg)](https://docs.rs/reactive-state/) [![license badge](https://img.shields.io/github/license/kellpossible/reactive-state)](https://github.com/kellpossible/reactive-state/blob/master/LICENSE.txt) [![github action badge](https://github.com/kellpossible/reactive-state/workflows/Rust/badge.svg)](https://github.com/kellpossible/reactive-state/actions?query=workflow%3ARust) 2 | 3 | This library is inspired by [redux](https://redux.js.org/), and designed to be 4 | used within Rust GUI applications to manage centralised global state which 5 | behaves in a predictable way. 6 | 7 | ## Usage 8 | 9 | ### Optional Features 10 | 11 | The following optional crate features can be enabled: 12 | 13 | + `"simple_logger"` - Logging middleware in the `simple_logger` module which uses the `log` macros. 14 | + `"web_logger"` - Logging middleware in the `web_logger` module, for applications running in the browser using [wasm-bindgen](https://crates.io/crates/wasm-bindgen). 15 | + `"yew"` - Support for compatibility trait implementations on [yew](https://crates.io/crates/yew) types. 16 | 17 | ## Middleware 18 | 19 | The behaviour of the system is customisable via middleware, and provided in this 20 | library are a couple of examples, a simple logger, and a web based logger 21 | inspired by [redux-logger](https://github.com/LogRocket/redux-logger). 22 | 23 | ![web_logger](./screenshots/reactive_state_20200601.png) 24 | *The `web_logger` Middleware* 25 | 26 | ### Available Middleware 27 | 28 | Aside from the built in middleware `web_logger` and `simple_logger` (available via crate features), here is a list of available middleware crates: 29 | 30 | + [`switch-router-middleware`](https://github.com/kellpossible/switch-router-middleware) - Routing middleware for [reactive-state](https://crates.io/crates/reactive-state), which makes use of [switch-router](https://github.com/kellpossible/switch-router). 31 | -------------------------------------------------------------------------------- /src/middleware/simple_logger.rs: -------------------------------------------------------------------------------- 1 | //! Logging [Middleware](crate::middleware::Middleware) which uses the 2 | //! [log](log) macros to publish actions/events that occur within the 3 | //! [Store](crate::Store). 4 | 5 | use super::ReduceMiddlewareResult; 6 | use crate::{ 7 | middleware::{Middleware, ReduceFn}, 8 | Store, 9 | }; 10 | use std::{fmt::Debug, hash::Hash}; 11 | 12 | pub enum LogLevel { 13 | Trace, 14 | Debug, 15 | Warn, 16 | Info, 17 | } 18 | 19 | impl LogLevel { 20 | pub fn log>(&self, message: S) { 21 | match self { 22 | LogLevel::Trace => log::trace!("{}", message.as_ref()), 23 | LogLevel::Debug => log::debug!("{}", message.as_ref()), 24 | LogLevel::Warn => log::warn!("{}", message.as_ref()), 25 | LogLevel::Info => log::info!("{}", message.as_ref()), 26 | } 27 | } 28 | } 29 | 30 | impl Default for LogLevel { 31 | fn default() -> Self { 32 | LogLevel::Debug 33 | } 34 | } 35 | 36 | /// Logging [Middleware](crate::middleware::Middleware) which uses the 37 | /// [log](log) macros to publish actions/events that occur within the 38 | /// [Store](crate::Store). 39 | /// 40 | /// See [simple_logger](super::simple_logger) for more details. 41 | pub struct SimpleLoggerMiddleware { 42 | log_level: LogLevel, 43 | } 44 | 45 | impl SimpleLoggerMiddleware { 46 | pub fn new() -> Self { 47 | SimpleLoggerMiddleware { 48 | log_level: LogLevel::default(), 49 | } 50 | } 51 | 52 | pub fn log_level(mut self, log_level: LogLevel) -> Self { 53 | self.log_level = log_level; 54 | self 55 | } 56 | } 57 | 58 | impl Default for SimpleLoggerMiddleware { 59 | fn default() -> Self { 60 | SimpleLoggerMiddleware::new() 61 | } 62 | } 63 | 64 | impl Middleware 65 | for SimpleLoggerMiddleware 66 | where 67 | Event: Clone + Hash + Eq + Debug, 68 | State: Debug, 69 | Action: Debug, 70 | Effect: Debug, 71 | { 72 | fn on_reduce( 73 | &self, 74 | store: &Store, 75 | action: Option<&Action>, 76 | reduce: ReduceFn, 77 | ) -> ReduceMiddlewareResult { 78 | let was_action = match &action { 79 | Some(action) => { 80 | self.log_level 81 | .log(format!("prev state: {:?}", store.state())); 82 | self.log_level.log(format!("action: {:?}", action)); 83 | true 84 | } 85 | None => { 86 | self.log_level.log("action: None"); 87 | false 88 | } 89 | }; 90 | 91 | let events = reduce(store, action); 92 | 93 | if was_action { 94 | self.log_level 95 | .log(format!("next state: {:?}", store.state())); 96 | } 97 | 98 | events 99 | } 100 | 101 | fn process_effect( 102 | &self, 103 | _store: &Store, 104 | effect: Effect, 105 | ) -> Option { 106 | self.log_level.log(format!("effect: {:?}", effect)); 107 | Some(effect) 108 | } 109 | 110 | fn on_notify( 111 | &self, 112 | store: &Store, 113 | events: Vec, 114 | notify: super::NotifyFn, 115 | ) -> Vec { 116 | for event in &events { 117 | self.log_level.log(format!("event: {:?}", event)); 118 | } 119 | 120 | notify(store, events) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | //! [Middleware] used to modify the behaviour of a [Store] during a 2 | //! [Store::dispatch()]. This module also contains some simple 3 | //! middleware implementations which can be used as utilities in an 4 | //! application. 5 | 6 | #[cfg(feature = "simple_logger")] 7 | #[cfg_attr(docsrs, doc(cfg(feature = "simple_logger")))] 8 | pub mod simple_logger; 9 | 10 | #[cfg(feature = "web_logger")] 11 | #[cfg_attr(docsrs, doc(cfg(feature = "web_logger")))] 12 | pub mod web_logger; 13 | 14 | use crate::Store; 15 | 16 | pub struct ReduceMiddlewareResult { 17 | pub events: Vec, 18 | pub effects: Vec, 19 | } 20 | 21 | impl Default for ReduceMiddlewareResult { 22 | fn default() -> Self { 23 | ReduceMiddlewareResult { 24 | events: Vec::new(), 25 | effects: Vec::new(), 26 | } 27 | } 28 | } 29 | 30 | /// Executes subsequent middleware and then runs the [Reducer](crate::Reducer). 31 | pub type ReduceFn = fn( 32 | &Store, 33 | Option<&Action>, 34 | ) -> ReduceMiddlewareResult; 35 | 36 | /// Executes subsequent middleware and then notifies the listeners. 37 | pub type NotifyFn = 38 | fn(&Store, Vec) -> Vec; 39 | 40 | /// `Middleware` used to modify the behaviour of a [Store] during a 41 | /// [Store::dispatch()]. 42 | pub trait Middleware { 43 | /// This method is invoked by the [Store] during a 44 | /// [Store::dispatch()] just before the `Action` is sent to the 45 | /// [Reducer](crate::Reducer). It is necessary to call the 46 | /// provided `reduce` function, which executes subsequent 47 | /// middleware and runs the [Reducer](crate::Reducer), and usually 48 | /// the events produced by the `reduce` function are returned from 49 | /// this method. 50 | /// 51 | /// This method allows modifying the action in question, or even 52 | /// removing it, preventing the [Reducer](crate::Reducer) from 53 | /// processing the action. It also allows modifying the events 54 | /// produced by the [Reducer](crate::Reducer) before the 55 | /// [Middleware::on_notify()] is invoked and they are sent to the 56 | /// [Store] listeners. 57 | fn on_reduce( 58 | &self, 59 | store: &Store, 60 | action: Option<&Action>, 61 | reduce: ReduceFn, 62 | ) -> ReduceMiddlewareResult { 63 | reduce(store, action) 64 | } 65 | 66 | /// Process an `Effect`. Returns `None` if the effect was 67 | /// processed/consumed by this handler, otherwise returns 68 | /// `Some(effect)`. 69 | fn process_effect( 70 | &self, 71 | _store: &Store, 72 | effect: Effect, 73 | ) -> Option { 74 | Some(effect) 75 | } 76 | 77 | /// This method is invoked by the [Store] during a 78 | /// [Store::dispatch()] after the [Reducer](crate::Reducer) has 79 | /// processed the `Action` and all [Middleware::on_reduce()] 80 | /// methods have completed, just before resulting events are 81 | /// sent to the store listeners. It is necessary to call the 82 | /// provided `notify` function, which executes subsequent 83 | /// middleware and then notifies the listeners. 84 | /// 85 | /// This method allows modifying the events in question before the 86 | /// listeners are notified. 87 | fn on_notify( 88 | &self, 89 | store: &Store, 90 | events: Vec, 91 | notify: NotifyFn, 92 | ) -> Vec { 93 | notify(store, events) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/listener.rs: -------------------------------------------------------------------------------- 1 | use std::rc::{Rc, Weak}; 2 | 3 | /// A trait to take a [Callback] or other custom callback type and 4 | /// produce a [Listener], a weak reference to that callback. 5 | pub trait AsListener { 6 | /// Produce a [Listener], a weak reference to this callback. 7 | fn as_listener(&self) -> Listener; 8 | } 9 | 10 | /// A weak reference to a callback function (usually [Callback]) which 11 | /// is notified of changes to [Store](crate::Store) `State`, and 12 | /// `Event`s produced by the store. 13 | /// 14 | /// See [Callback](Callback) for more information about how this is 15 | /// typically used. 16 | #[derive(Clone)] 17 | pub struct Listener(Weak, Option)>); 18 | 19 | impl Listener { 20 | /// Attempt to upgrade the weak reference in this listener to a 21 | /// [Callback], otherwise if unable to, returns `None`. 22 | pub fn as_callback(&self) -> Option> { 23 | match self.0.upgrade() { 24 | Some(listener_rc) => Some(Callback(listener_rc)), 25 | None => None, 26 | } 27 | } 28 | } 29 | 30 | impl AsListener for Listener { 31 | fn as_listener(&self) -> Listener { 32 | Listener(self.0.clone()) 33 | } 34 | } 35 | 36 | /// A wrapper for a callback which is notified of changes to 37 | /// [Store](crate::Store) `State`, and `Event`s produced by the store. 38 | /// 39 | /// ## Example 40 | /// 41 | /// The following example makes use of the [AsListener](AsListener) 42 | /// trait implementation for `Callback` which allows it to be used in 43 | /// [Store::subscribe()](crate::Store::subscribe). The 44 | /// [AsListener](AsListener) trait creates a weak reference to this 45 | /// callback in a [Listener](Listener), which is given to the 46 | /// [Store](crate::Store). When the callback is dropped, the listener will be 47 | /// removed from the store. 48 | /// 49 | /// ``` 50 | /// # use reactive_state::{ReducerFn, Store, ReducerResult}; 51 | /// # let reducer: ReducerFn<(), (), (), ()> = |_state, _action| { ReducerResult::default() }; 52 | /// # let store = Store::new(reducer, ()); 53 | /// use reactive_state::Callback; 54 | /// 55 | /// let callback = Callback::new(|_state, _event| { 56 | /// println!("Callback invoked"); 57 | /// }); 58 | /// 59 | /// store.subscribe(&callback); 60 | /// ``` 61 | /// 62 | /// ## Optional Features 63 | /// 64 | /// If the `"yew"` crate feature is enabled, a number of `From` 65 | /// implementations are available to convert `yew` callbacks into 66 | /// this: 67 | /// 68 | /// + `From>>` 69 | /// + `From, Event)>>` 70 | /// + `From>` 71 | #[derive(Clone)] 72 | pub struct Callback(Rc, Option)>); 73 | 74 | impl AsListener for &Callback { 75 | fn as_listener(&self) -> Listener { 76 | Listener(Rc::downgrade(&self.0)) 77 | } 78 | } 79 | 80 | impl Callback { 81 | pub fn new, Option) + 'static>(closure: C) -> Self { 82 | Callback(Rc::new(closure)) 83 | } 84 | pub fn emit(&self, state: Rc, event: Option) { 85 | (self.0)(state, event) 86 | } 87 | } 88 | 89 | impl From for Callback 90 | where 91 | C: Fn(Rc, Option) + 'static, 92 | { 93 | fn from(closure: C) -> Self { 94 | Callback(Rc::new(closure)) 95 | } 96 | } 97 | 98 | #[cfg(feature = "yew")] 99 | #[cfg_attr(docsrs, doc(cfg(feature = "yew")))] 100 | impl From>> for Callback 101 | where 102 | State: 'static, 103 | Event: 'static, 104 | { 105 | fn from(yew_callback: yew::Callback>) -> Self { 106 | Callback(Rc::new(move |state, _| { 107 | yew_callback.emit(state); 108 | })) 109 | } 110 | } 111 | 112 | #[cfg(feature = "yew")] 113 | #[cfg_attr(docsrs, doc(cfg(feature = "yew")))] 114 | impl From, Option)>> for Callback 115 | where 116 | State: 'static, 117 | Event: 'static, 118 | { 119 | fn from(yew_callback: yew::Callback<(Rc, Option)>) -> Self { 120 | Callback(Rc::new(move |state, event| { 121 | yew_callback.emit((state, event)); 122 | })) 123 | } 124 | } 125 | 126 | #[cfg(feature = "yew")] 127 | #[cfg_attr(docsrs, doc(cfg(feature = "yew")))] 128 | impl From> for Callback 129 | where 130 | State: 'static, 131 | Event: 'static, 132 | { 133 | fn from(yew_callback: yew::Callback<()>) -> Self { 134 | Callback(Rc::new(move |_, _| { 135 | yew_callback.emit(()); 136 | })) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This library is inspired by [redux](https://redux.js.org/), and 2 | //! designed to be used within Rust GUI applications to manage 3 | //! centralised global state which behaves in a predictable way. 4 | //! 5 | //! ## Example 6 | //! 7 | //! The following is a simple example of how to use this system to 8 | //! manage state, and subscribe to changes to state. 9 | //! 10 | //! ``` 11 | //! use reactive_state::{ReducerFn, ReducerResult, Store, Callback}; 12 | //! use std::{cell::RefCell, rc::Rc}; 13 | //! 14 | //! /// Something to hold the application state. 15 | //! #[derive(Clone)] 16 | //! struct MyState { 17 | //! pub variable: u32 18 | //! } 19 | //! 20 | //! /// Some actions to perform which alter the state. 21 | //! enum MyAction { 22 | //! Increment, 23 | //! Decrement 24 | //! } 25 | //! 26 | //! /// Some events that are fired during certain action/state combinations. 27 | //! #[derive(Clone, Eq, PartialEq, Hash)] 28 | //! enum MyEvent { 29 | //! IsOne, 30 | //! } 31 | //! 32 | //! /// A reducer to perform the actions, alter the state, and fire off events. 33 | //! let reducer: ReducerFn = |state, action| { 34 | //! let mut events: Vec = Vec::new(); 35 | //! 36 | //! let new_state = match action { 37 | //! MyAction::Increment => { 38 | //! let mut new_state = MyState::clone(state); 39 | //! new_state.variable = state.variable + 1; 40 | //! Rc::new(new_state) 41 | //! } 42 | //! MyAction::Decrement => { 43 | //! let mut new_state = MyState::clone(state); 44 | //! new_state.variable = state.variable - 1; 45 | //! Rc::new(new_state) 46 | //! } 47 | //! }; 48 | //! 49 | //! if new_state.variable == 1 { 50 | //! events.push(MyEvent::IsOne); 51 | //! } 52 | //! 53 | //! ReducerResult { 54 | //! state: new_state, 55 | //! events, 56 | //! effects: vec![], 57 | //! } 58 | //! }; 59 | //! 60 | //! // Set the initial state. 61 | //! let initial_state = MyState { 62 | //! variable: 0u32 63 | //! }; 64 | //! 65 | //! // Create the store. 66 | //! let store = Store::new(reducer, initial_state); 67 | //! 68 | //! // A test variable that will be altered by the callback. 69 | //! let callback_invokes: Rc> = Rc::new(RefCell::new(0u32)); 70 | //! let callback_invokes_local = callback_invokes.clone(); 71 | //! 72 | //! let callback = Callback::new(move |_state: Rc, _event: Option| { 73 | //! *(callback_invokes_local.borrow_mut()) += 1; 74 | //! }); 75 | //! 76 | //! // Subscribe to state changes which produce the IsOne event. 77 | //! store.subscribe_event(&callback, MyEvent::IsOne); 78 | //! 79 | //! assert_eq!(0, store.state().variable); 80 | //! assert_eq!(0, *RefCell::borrow(&callback_invokes)); 81 | //! 82 | //! // Dispatch an increment action onto the store, which will 83 | //! // alter the state. 84 | //! store.dispatch(MyAction::Increment); 85 | //! 86 | //! // The state has been altered. 87 | //! assert_eq!(1, store.state().variable); 88 | //! 89 | //! // The callback was invoked. 90 | //! assert_eq!(1, *RefCell::borrow(&callback_invokes)); 91 | //! 92 | //! store.dispatch(MyAction::Increment); 93 | //! 94 | //! // The state has been altered. 95 | //! assert_eq!(2, store.state().variable); 96 | //! 97 | //! // The callback was not invoked, because the event IsOne 98 | //! // was not fired by the reducer. 99 | //! assert_eq!(1, *RefCell::borrow(&callback_invokes)); 100 | //! 101 | //! // Drop the callback, and it will also be removed from the store. 102 | //! drop(callback); 103 | //! 104 | //! store.dispatch(MyAction::Decrement); 105 | //! 106 | //! // The state has been altered again. 107 | //! assert_eq!(1, store.state().variable); 108 | //! 109 | //! // The callback was dropped before the action was dispatched, 110 | //! // and so it was not invoked. 111 | //! assert_eq!(1, *RefCell::borrow(&callback_invokes)); 112 | //! ``` 113 | //! 114 | //! ## Side Effects 115 | //! 116 | //! Something that wasn't covered in the example above, was the 117 | //! concept of side effects produced in the reducer. This is the 118 | //! fourth type parameter `Effect` on [Store](Store), and effects 119 | //! which are produced in the reducer are given to the store via the 120 | //! [ReducerResult](ReducerResult) that it returns. Side effects are 121 | //! designed to be executed/handled by store [middleware](middleware). 122 | //! 123 | //! ## Optional Features 124 | //! 125 | //! The following optional crate features can be enabled: 126 | //! 127 | //! + `"simple_logger"` - Logging middleware in the 128 | //! [simple_logger](crate::middleware::simple_logger) module which 129 | //! uses the `log` macros. 130 | //! + `"web_logger"` - Logging middleware in the 131 | //! [web_logger](crate::middleware::web_logger) module, for 132 | //! applications running in the browser using 133 | //! [wasm-bindgen](https://crates.io/crates/wasm-bindgen). 134 | //! + `"yew"` - Support for compatibility trait implementations on 135 | //! [yew](https://crates.io/crates/yew) types. 136 | 137 | #![cfg_attr(docsrs, feature(doc_cfg))] 138 | 139 | mod listener; 140 | pub mod middleware; 141 | mod reducer; 142 | mod store; 143 | 144 | #[cfg(feature = "yew")] 145 | #[cfg_attr(docsrs, doc(cfg(feature = "yew")))] 146 | pub mod provider; 147 | 148 | pub use listener::*; 149 | pub use reducer::*; 150 | pub use store::{Store, StoreRef}; 151 | -------------------------------------------------------------------------------- /src/provider.rs: -------------------------------------------------------------------------------- 1 | use crate::Store; 2 | use std::{cell::RefCell, fmt::Debug, rc::Rc}; 3 | use yew::{ 4 | html, html::ChildrenRenderer, virtual_dom::VChild, ChildrenWithProps, Component, ComponentLink, 5 | Properties, 6 | }; 7 | 8 | #[derive(Clone)] 9 | pub struct MapStateToProps( 10 | fn(&Rc, &C::Properties) -> Option, 11 | ); 12 | 13 | impl PartialEq for MapStateToProps 14 | where 15 | C: Component, 16 | { 17 | fn eq(&self, other: &MapStateToProps) -> bool { 18 | (self.0 as *const ()) == (other.0 as *const ()) 19 | } 20 | } 21 | 22 | impl MapStateToProps 23 | where 24 | C: Component, 25 | { 26 | pub fn new(function: fn(&Rc, &C::Properties) -> Option) -> Self { 27 | Self(function) 28 | } 29 | 30 | pub fn perform(&self, state: &Rc, props: &C::Properties) -> Option { 31 | (self.0)(state, props) 32 | } 33 | } 34 | 35 | impl Debug for MapStateToProps { 36 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 37 | write!(f, "MapStateToProps(function @ {:p})", &self.0) 38 | } 39 | } 40 | 41 | #[derive(Clone, Properties)] 42 | struct Props 43 | where 44 | C: Component + Clone, 45 | C::Properties: PartialEq, 46 | State: Clone, 47 | Action: Clone, 48 | { 49 | pub map_state_to_props: MapStateToProps, 50 | pub store: Rc>>, 51 | pub children: ChildrenWithProps, 52 | } 53 | 54 | impl Debug for Props 55 | where 56 | C: Component + Clone, 57 | C::Properties: PartialEq, 58 | State: Clone, 59 | Action: Clone, 60 | { 61 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 62 | write!( 63 | f, 64 | "Provider::Props{{map_state_to_props: {0:?}, store @ {1:p}, children: {2:?}}}", 65 | self.map_state_to_props, &*self.store, self.children 66 | ) 67 | } 68 | } 69 | 70 | impl PartialEq for Props 71 | where 72 | C: Component + Clone, 73 | C::Properties: PartialEq, 74 | State: Clone, 75 | Action: Clone, 76 | { 77 | fn eq(&self, other: &Props) -> bool { 78 | // TODO: this should also include the children, but it's not currently possible due to https://github.com/yewstack/yew/issues/1216 79 | Rc::ptr_eq(&self.store, &other.store) 80 | && self.map_state_to_props == other.map_state_to_props 81 | && self.children == other.children 82 | } 83 | } 84 | 85 | enum Msg { 86 | StateUpdate(Rc), 87 | } 88 | 89 | struct Provider 90 | where 91 | C: Component + Clone, 92 | C::Properties: PartialEq, 93 | State: Clone + 'static, 94 | Action: Clone + 'static, 95 | Event: Clone + 'static, 96 | { 97 | props: Props, 98 | children: ChildrenWithProps, 99 | _link: ComponentLink>, 100 | _callback: crate::Callback, 101 | } 102 | 103 | impl Provider 104 | where 105 | C: Component + Clone, 106 | C::Properties: PartialEq, 107 | State: Clone + 'static, 108 | Action: Clone + 'static, 109 | Event: Clone + 'static, 110 | { 111 | fn update_children_props( 112 | children: &ChildrenWithProps, 113 | state: &Rc, 114 | map_state_to_props: &MapStateToProps, 115 | ) -> Option> { 116 | // TODO: only make the children vec if props changed 117 | // alternatively request an iter_mut implementation for ChildrenWithProps... 118 | let mut children_vec: Vec> = children.iter().collect(); 119 | let mut child_props_changed = false; 120 | 121 | for child in &mut children_vec { 122 | if let Some(properties) = map_state_to_props.perform(state, &child.props) { 123 | child.props = properties; 124 | child_props_changed = true; 125 | } 126 | } 127 | 128 | if child_props_changed { 129 | Some(ChildrenRenderer::new(children_vec)) 130 | } else { 131 | None 132 | } 133 | } 134 | } 135 | 136 | impl Component for Provider 137 | where 138 | C: Component + Clone, 139 | C::Properties: PartialEq, 140 | State: Clone + 'static, 141 | Action: Clone + 'static, 142 | Event: Clone + 'static, 143 | { 144 | type Message = Msg; 145 | type Properties = Props; 146 | 147 | fn create(props: Props, link: yew::ComponentLink) -> Self { 148 | let callback = link.callback(|(state, _)| Msg::StateUpdate(state)).into(); 149 | 150 | let children = match Self::update_children_props( 151 | &props.children, 152 | &props.store.borrow().state(), 153 | &props.map_state_to_props, 154 | ) { 155 | None => props.children.clone(), 156 | Some(children) => children, 157 | }; 158 | 159 | Self { 160 | props, 161 | children, 162 | _link: link, 163 | _callback: callback, 164 | } 165 | } 166 | 167 | fn update(&mut self, msg: Msg) -> yew::ShouldRender { 168 | match msg { 169 | Msg::StateUpdate(state) => { 170 | let result: Option> = Self::update_children_props( 171 | &self.props.children, 172 | &state, 173 | &self.props.map_state_to_props, 174 | ); 175 | match result { 176 | Some(new_children) => { 177 | self.children = new_children; 178 | true 179 | } 180 | None => false, 181 | } 182 | } 183 | } 184 | } 185 | 186 | fn change(&mut self, props: Props) -> yew::ShouldRender { 187 | if self.props != props { 188 | if self.props.children != props.children { 189 | match Self::update_children_props( 190 | &props.children, 191 | &props.store.borrow().state(), 192 | &props.map_state_to_props, 193 | ) { 194 | None => self.children = props.children.clone(), 195 | Some(children) => self.children = children, 196 | }; 197 | } 198 | 199 | self.props = props; 200 | true 201 | } else { 202 | false 203 | } 204 | } 205 | 206 | fn view(&self) -> yew::Html { 207 | html! { <>{ self.children.clone() } } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/reducer.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | /// A wrapper for a function that implements the [Reducer](Reducer) 4 | /// trait. 5 | /// 6 | /// ## Example 7 | /// 8 | /// ``` 9 | /// # #[derive(Clone)] 10 | /// # struct MyState { 11 | /// # pub variable: bool 12 | /// # } 13 | /// # 14 | /// # enum MyAction { 15 | /// # SomeAction 16 | /// # } 17 | /// # 18 | /// # enum MyEvent { 19 | /// # SomeEvent 20 | /// # } 21 | /// # 22 | /// # enum MyEffect { 23 | /// # SomeEffect 24 | /// # } 25 | /// use reactive_state::{ReducerFn, ReducerResult, Reducer}; 26 | /// use std::rc::Rc; 27 | /// 28 | /// let reducer: ReducerFn = |state, action| { 29 | /// let mut events = Vec::new(); 30 | /// 31 | /// let new_state = match action { 32 | /// MyAction::SomeAction => { 33 | /// // create a new state to mutate and return 34 | /// let mut new_state = MyState::clone(state); 35 | /// new_state.variable = true; 36 | /// 37 | /// // An event needs to be produced to notify 38 | /// // subscribers that the state has changed. 39 | /// events.push(MyEvent::SomeEvent); 40 | /// 41 | /// Rc::new(new_state) 42 | /// } 43 | /// }; 44 | /// 45 | /// ReducerResult { 46 | /// state: new_state, 47 | /// events: events, 48 | /// effects: vec![], 49 | /// } 50 | /// }; 51 | /// 52 | /// let state1 = Rc::new(MyState { 53 | /// variable: false 54 | /// }); 55 | /// 56 | /// let result = reducer.reduce(&state1, &MyAction::SomeAction); 57 | /// let state2 = &result.state; 58 | /// 59 | /// assert_eq!(false, state1.variable); 60 | /// assert_eq!(true, state2.variable); 61 | /// ``` 62 | /// 63 | /// For a more comprehensive example of how reducers are used in the 64 | /// context of the entire system, see [reactive_state](crate). 65 | pub type ReducerFn = 66 | fn(&Rc, &Action) -> ReducerResult; 67 | 68 | impl Reducer for T 69 | where 70 | T: Fn(&Rc, &Action) -> ReducerResult, 71 | { 72 | fn reduce( 73 | &self, 74 | prev_state: &Rc, 75 | action: &Action, 76 | ) -> ReducerResult { 77 | (self)(prev_state, action) 78 | } 79 | } 80 | 81 | /// Using the [reduce()](Reducer::reduce()) method, implementors of 82 | /// this trait take an `Action` submitted to a store via 83 | /// [Store::dispatch()](crate::Store::dispatch()) and modifies the 84 | /// `State` in the store, producing a new `State`, and also producing 85 | /// events and effects associated with the `Action` and state 86 | /// modifications that occurred. 87 | /// 88 | /// For an example of how a reducer function should work, see 89 | /// [ReducerFn](ReducerFn). For an example of how to use one in 90 | /// conjunction with a [Store](crate::Store), see 91 | /// [reactive_state](crate). 92 | pub trait Reducer { 93 | /// Take an `Action` submitted to a store via 94 | /// [Store::dispatch()](crate::Store::dispatch()) and modifies the 95 | /// `prev_state`, producing a new `State`, and also producing 96 | /// events associated with the `Action` and state modifications 97 | /// that occurred. 98 | /// 99 | /// **Note:** If no `Event`s are returned then it is assumed that 100 | /// the state has not changed, and store listeners do not need to 101 | /// be notified. 102 | /// 103 | /// **Note:** If all `Events`s are `Event::none()`, then it is 104 | /// also assumed that the state has not changed, and store 105 | /// listeners do not need to be notified. 106 | /// 107 | /// This method should be a pure function, with any required side 108 | /// effects being emmitted via the returned 109 | /// [ReducerResult](ReducerResult). 110 | /// 111 | /// `Events`s should generally be treated purely as a notification 112 | /// that some subset of the state has been modified, such that 113 | /// playing the events and state transitions in reverse will 114 | /// result in the same application behaviour. 115 | /// 116 | /// `Effect`s are side effects invoked as a result of the action, 117 | /// these may involve dispatching further actions, or modifying 118 | /// some other part of the system that the store is involved with. 119 | /// `Effect`s are processed using 120 | /// [Middleware](crate::middleware::Middleware) which has been 121 | /// added to the [Store](crate::Store). 122 | fn reduce( 123 | &self, 124 | prev_state: &Rc, 125 | action: &Action, 126 | ) -> ReducerResult; 127 | } 128 | 129 | /// The result of a [Reducer::reduce()] function. 130 | /// 131 | /// `Events`s should generally be treated purely as a notification 132 | /// that some subset of the state has been modified, such that 133 | /// playing the events and state transitions in reverse will 134 | /// result in the same application behaviour. 135 | /// 136 | /// `Effect`s are side effects invoked as a result of the action, 137 | /// these may involve dispatching further actions, or modifying 138 | /// some other part of the system that the store is involved with. 139 | /// `Effect`s are processed using [Middleware](crate::middleware::Middleware) 140 | /// which has been added to the [Store](crate::Store). 141 | pub struct ReducerResult { 142 | pub state: Rc, 143 | pub events: Vec, 144 | pub effects: Vec, 145 | } 146 | 147 | impl Default for ReducerResult 148 | where 149 | State: Default, 150 | { 151 | fn default() -> Self { 152 | Self { 153 | state: Rc::new(State::default()), 154 | events: vec![], 155 | effects: vec![], 156 | } 157 | } 158 | } 159 | 160 | // TODO: create a zero cost macro version of this #17 161 | /// A [Reducer] composed of multiple reducers. 162 | pub struct CompositeReducer { 163 | reducers: Vec>>, 164 | } 165 | 166 | impl CompositeReducer { 167 | /// Create a new [CompositeReducer]. 168 | pub fn new(reducers: Vec>>) -> Self { 169 | CompositeReducer { reducers } 170 | } 171 | } 172 | 173 | impl Reducer 174 | for CompositeReducer 175 | { 176 | fn reduce( 177 | &self, 178 | prev_state: &Rc, 179 | action: &Action, 180 | ) -> ReducerResult { 181 | let mut sum_result: ReducerResult = ReducerResult { 182 | state: prev_state.clone(), 183 | events: Vec::new(), 184 | effects: Vec::new(), 185 | }; 186 | 187 | for reducer in &self.reducers { 188 | let result = reducer.reduce(&sum_result.state, action); 189 | sum_result.state = result.state; 190 | sum_result.events.extend(result.events); 191 | sum_result.effects.extend(result.effects); 192 | } 193 | 194 | sum_result 195 | } 196 | } 197 | 198 | #[cfg(test)] 199 | mod tests { 200 | use crate::{CompositeReducer, Reducer, ReducerResult}; 201 | use std::rc::Rc; 202 | 203 | struct TestState { 204 | emitted_events: Vec, 205 | } 206 | 207 | impl Default for TestState { 208 | fn default() -> Self { 209 | TestState { 210 | emitted_events: Vec::new(), 211 | } 212 | } 213 | } 214 | 215 | struct TestAction; 216 | 217 | #[derive(Debug, Clone, PartialEq)] 218 | enum TestEvent { 219 | Event1, 220 | Event2, 221 | } 222 | 223 | #[derive(Debug, PartialEq)] 224 | enum TestEffect { 225 | Effect1, 226 | Effect2, 227 | } 228 | 229 | struct Reducer1; 230 | 231 | impl Reducer for Reducer1 { 232 | fn reduce( 233 | &self, 234 | prev_state: &Rc, 235 | _action: &TestAction, 236 | ) -> crate::ReducerResult { 237 | let mut emitted_events = prev_state.emitted_events.clone(); 238 | emitted_events.push(TestEvent::Event1); 239 | let state = Rc::new(TestState { emitted_events }); 240 | 241 | ReducerResult { 242 | state, 243 | events: vec![TestEvent::Event1], 244 | effects: vec![TestEffect::Effect1], 245 | } 246 | } 247 | } 248 | 249 | struct Reducer2; 250 | 251 | impl Reducer for Reducer2 { 252 | fn reduce( 253 | &self, 254 | prev_state: &Rc, 255 | _action: &TestAction, 256 | ) -> crate::ReducerResult { 257 | let mut emitted_events = prev_state.emitted_events.clone(); 258 | emitted_events.push(TestEvent::Event2); 259 | let state = Rc::new(TestState { emitted_events }); 260 | 261 | ReducerResult { 262 | state, 263 | events: vec![TestEvent::Event2], 264 | effects: vec![TestEffect::Effect2], 265 | } 266 | } 267 | } 268 | 269 | #[test] 270 | fn composite_reducer() { 271 | let reducer = CompositeReducer::new(vec![Box::new(Reducer1), Box::new(Reducer2)]); 272 | 273 | let result = reducer.reduce(&Rc::new(TestState::default()), &TestAction); 274 | assert_eq!( 275 | result.state.emitted_events, 276 | vec![TestEvent::Event1, TestEvent::Event2] 277 | ); 278 | assert_eq!(result.events, vec![TestEvent::Event1, TestEvent::Event2]); 279 | assert_eq!( 280 | result.effects, 281 | vec![TestEffect::Effect1, TestEffect::Effect2] 282 | ); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/middleware/web_logger.rs: -------------------------------------------------------------------------------- 1 | //! Logging [Middleware](crate::middleware::Middleware) for 2 | //! applications running in the browser using `wasm-bindgen`. 3 | //! Publishes actions/events that occur within the 4 | //! [Store](crate::Store). 5 | 6 | use super::{Middleware, ReduceMiddlewareResult}; 7 | use serde::Serialize; 8 | use std::{fmt::Display, hash::Hash}; 9 | use wasm_bindgen::JsValue; 10 | use web_sys::console; 11 | 12 | pub enum LogLevel { 13 | Trace, 14 | Debug, 15 | Warn, 16 | Info, 17 | Log, 18 | } 19 | 20 | impl LogLevel { 21 | pub fn log_1(&self, message: &JsValue) { 22 | #[allow(unused_unsafe)] 23 | unsafe { 24 | match self { 25 | LogLevel::Trace => console::trace_1(message), 26 | LogLevel::Debug => console::debug_1(message), 27 | LogLevel::Warn => console::warn_1(message), 28 | LogLevel::Info => console::info_1(message), 29 | LogLevel::Log => console::log_1(message), 30 | } 31 | } 32 | } 33 | 34 | pub fn log(&self, messages: Vec) { 35 | let messages_array = js_sys::Array::new_with_length(messages.len() as u32); 36 | 37 | for (i, m) in messages.into_iter().enumerate() { 38 | messages_array.set(i as u32, m); 39 | } 40 | 41 | #[allow(unused_unsafe)] 42 | unsafe { 43 | match self { 44 | LogLevel::Trace => console::trace(&messages_array), 45 | LogLevel::Debug => console::debug(&messages_array), 46 | LogLevel::Warn => console::warn(&messages_array), 47 | LogLevel::Info => console::info(&messages_array), 48 | LogLevel::Log => console::log(&messages_array), 49 | } 50 | } 51 | } 52 | } 53 | 54 | pub enum DisplayType { 55 | /// Print using the browser's log groups. Unfortunately this isn't 56 | /// always very consistent, especially with 57 | /// asynchronous/concurrent events. 58 | Groups, 59 | /// Print the data in a single javascript object tree. 60 | SingleObject, 61 | } 62 | 63 | impl Default for DisplayType { 64 | fn default() -> Self { 65 | Self::Groups 66 | } 67 | } 68 | 69 | #[derive(Serialize)] 70 | struct OnReduceLog<'a, State, Action, Effect> { 71 | action: &'a Option, 72 | prev_state: &'a State, 73 | next_state: &'a State, 74 | effects: &'a [Effect], 75 | } 76 | 77 | #[derive(Serialize)] 78 | struct OnNotifyLog<'a, State, Event> { 79 | state: &'a State, 80 | events: &'a [Event], 81 | } 82 | 83 | impl Default for LogLevel { 84 | fn default() -> Self { 85 | LogLevel::Log 86 | } 87 | } 88 | 89 | /// Logging middleware for applications running in the browser. 90 | /// 91 | /// See [web_logger](super::web_logger) for more details. 92 | pub struct WebLoggerMiddleware { 93 | log_level: LogLevel, 94 | display_type: DisplayType, 95 | } 96 | 97 | impl WebLoggerMiddleware { 98 | pub fn new() -> Self { 99 | Self { 100 | log_level: LogLevel::default(), 101 | display_type: DisplayType::default(), 102 | } 103 | } 104 | 105 | /// Set the level at which the data from this middleware will be 106 | /// logged to. 107 | pub fn log_level(mut self, log_level: LogLevel) -> Self { 108 | self.log_level = log_level; 109 | self 110 | } 111 | 112 | /// What type of display to use when printing the data from this 113 | /// middleware. 114 | pub fn display_type(mut self, display_type: DisplayType) -> Self { 115 | self.display_type = display_type; 116 | self 117 | } 118 | 119 | fn on_reduce_groups( 120 | &self, 121 | store: &crate::Store, 122 | action: Option<&Action>, 123 | reduce: super::ReduceFn, 124 | ) -> ReduceMiddlewareResult 125 | where 126 | State: Serialize, 127 | Action: Serialize + Display, 128 | Event: Clone + Hash + Eq + Serialize, 129 | Effect: Serialize, 130 | { 131 | let prev_state_js = JsValue::from_serde(&(*store.state())).unwrap(); 132 | 133 | let action_js = JsValue::from_serde(&action).unwrap(); 134 | let action_display = match &action { 135 | Some(action) => format!("{}", action), 136 | None => "None".to_string(), 137 | }; 138 | 139 | let result = reduce(store, action); 140 | let next_state_js = JsValue::from_serde(&(*store.state())).unwrap(); 141 | 142 | let effects_js = JsValue::from_serde(&result.effects).unwrap(); 143 | let effects_display = match &result.effects.len() { 144 | 0 => "None".to_string(), 145 | _ => format!("({})", result.effects.len()), 146 | }; 147 | 148 | #[allow(unused_unsafe)] 149 | unsafe { 150 | console::group_collapsed_3( 151 | &JsValue::from_serde(&format!("%caction %c{}", action_display)).unwrap(), 152 | &JsValue::from_str("color: gray; font-weight: lighter;"), 153 | &JsValue::from_str("inherit"), 154 | ); 155 | console::group_collapsed_2( 156 | &JsValue::from_str("%cprev state"), 157 | &JsValue::from_str("color: #9E9E9E; font-weight: bold;"), 158 | ); 159 | } 160 | 161 | self.log_level.log_1(&prev_state_js); 162 | 163 | #[allow(unused_unsafe)] 164 | unsafe { 165 | console::group_end(); 166 | 167 | console::group_collapsed_3( 168 | &JsValue::from_str(&format!("%caction: %c{}", action_display)), 169 | &JsValue::from_str("color: #03A9F4; font-weight: bold;"), 170 | &JsValue::from_str("color: gray; font-weight: lighter;"), 171 | ); 172 | } 173 | 174 | self.log_level.log_1(&action_js); 175 | 176 | #[allow(unused_unsafe)] 177 | unsafe { 178 | console::group_end(); 179 | 180 | console::group_collapsed_2( 181 | &JsValue::from_str("%cnext state"), 182 | &JsValue::from_str("color: #4CAF50; font-weight: bold;"), 183 | ); 184 | } 185 | 186 | self.log_level.log_1(&next_state_js); 187 | 188 | #[allow(unused_unsafe)] 189 | unsafe { 190 | console::group_end(); 191 | 192 | console::group_collapsed_3( 193 | &JsValue::from_str(&format!("%ceffects: %c{}", effects_display)), 194 | &JsValue::from_str("color: #C210C2; font-weight: bold;"), 195 | &JsValue::from_str("color: gray; font-weight: lighter;"), 196 | ); 197 | } 198 | self.log_level.log_1(&effects_js); 199 | 200 | #[allow(unused_unsafe)] 201 | unsafe { 202 | console::group_end(); 203 | } 204 | 205 | result 206 | } 207 | 208 | fn on_reduce_no_groups( 209 | &self, 210 | store: &crate::Store, 211 | action: Option<&Action>, 212 | reduce: super::ReduceFn, 213 | ) -> ReduceMiddlewareResult 214 | where 215 | State: Serialize, 216 | Action: Serialize + Display, 217 | Event: Clone + Hash + Eq + Serialize, 218 | Effect: Serialize, 219 | { 220 | let action_display = format!( 221 | "on_reduce(), action: {}", 222 | match &action { 223 | Some(action) => format!("{}", action), 224 | None => "None".to_string(), 225 | } 226 | ); 227 | 228 | let action_display_js = JsValue::from_str(&action_display); 229 | 230 | let prev_state = store.state(); 231 | 232 | let result = reduce(store, action); 233 | let next_state = store.state(); 234 | 235 | let log_object = OnReduceLog { 236 | action: &action, 237 | prev_state: &*prev_state, 238 | next_state: &*next_state, 239 | effects: &result.effects, 240 | }; 241 | 242 | let log_object_js = JsValue::from_serde(&log_object).unwrap(); 243 | self.log_level.log(vec![action_display_js, log_object_js]); 244 | 245 | result 246 | } 247 | 248 | fn on_notify_groups( 249 | &self, 250 | store: &crate::Store, 251 | events: Vec, 252 | notify: super::NotifyFn, 253 | ) -> Vec 254 | where 255 | Event: Serialize, 256 | { 257 | let events_js = JsValue::from_serde(&events).unwrap(); 258 | let events_display = match events.len() { 259 | 0 => "None".to_string(), 260 | _ => format!("({})", events.len()), 261 | }; 262 | 263 | #[allow(unused_unsafe)] 264 | unsafe { 265 | console::group_collapsed_3( 266 | &JsValue::from_str(&format!("%cevents: %c{}", events_display)), 267 | &JsValue::from_str("color: #FCBA03; font-weight: bold;"), 268 | &JsValue::from_str("color: gray; font-weight: lighter;"), 269 | ); 270 | } 271 | 272 | self.log_level.log_1(&events_js); 273 | 274 | #[allow(unused_unsafe)] 275 | unsafe { 276 | console::group_end(); 277 | console::group_end(); 278 | } 279 | 280 | notify(store, events) 281 | } 282 | 283 | fn on_notify_no_groups( 284 | &self, 285 | store: &crate::Store, 286 | events: Vec, 287 | notify: super::NotifyFn, 288 | ) -> Vec 289 | where 290 | Event: Serialize + Clone + Hash + Eq, 291 | State: Serialize, 292 | { 293 | let log_object = OnNotifyLog { 294 | state: &*store.state(), 295 | events: &events, 296 | }; 297 | 298 | let log_object_js = JsValue::from_serde(&log_object).unwrap(); 299 | 300 | let display = JsValue::from_str("on_notify(): "); 301 | 302 | self.log_level.log(vec![display, log_object_js]); 303 | 304 | notify(store, events) 305 | } 306 | } 307 | 308 | impl Default for WebLoggerMiddleware { 309 | fn default() -> Self { 310 | WebLoggerMiddleware::new() 311 | } 312 | } 313 | 314 | impl Middleware for WebLoggerMiddleware 315 | where 316 | State: Serialize, 317 | Action: Serialize + Display, 318 | Event: Clone + Hash + Eq + Serialize, 319 | Effect: Serialize, 320 | { 321 | fn on_reduce( 322 | &self, 323 | store: &crate::Store, 324 | action: Option<&Action>, 325 | reduce: super::ReduceFn, 326 | ) -> ReduceMiddlewareResult { 327 | match self.display_type { 328 | DisplayType::Groups => self.on_reduce_groups(store, action, reduce), 329 | DisplayType::SingleObject => self.on_reduce_no_groups(store, action, reduce), 330 | } 331 | } 332 | 333 | fn process_effect( 334 | &self, 335 | _store: &crate::Store, 336 | effect: Effect, 337 | ) -> Option { 338 | Some(effect) 339 | } 340 | 341 | fn on_notify( 342 | &self, 343 | store: &crate::Store, 344 | events: Vec, 345 | notify: super::NotifyFn, 346 | ) -> Vec { 347 | match self.display_type { 348 | DisplayType::Groups => self.on_notify_groups(store, events, notify), 349 | DisplayType::SingleObject => self.on_notify_no_groups(store, events, notify), 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/store.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | middleware::{Middleware, ReduceMiddlewareResult}, 3 | AsListener, Listener, Reducer, 4 | }; 5 | use std::iter::FromIterator; 6 | use std::ops::Deref; 7 | use std::{ 8 | cell::{Cell, RefCell}, 9 | collections::{HashSet, VecDeque}, 10 | fmt::Debug, 11 | hash::Hash, 12 | marker::PhantomData, 13 | rc::Rc, 14 | }; 15 | 16 | /// A [Listener] associated with (listening to) a given set of 17 | /// `Events`s produced by a [Store::dispatch()]. 18 | struct ListenerEventPair { 19 | pub listener: Listener, 20 | pub events: HashSet, 21 | } 22 | 23 | impl Debug for ListenerEventPair { 24 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 | write!(f, "ListenerEventPair") 26 | } 27 | } 28 | 29 | /// An action to modify some aspect of the [Store], to be stored in a 30 | /// queue and executed at the start of a [Store::dispatch()] for a 31 | /// given `Action`. 32 | enum StoreModification { 33 | AddListener(ListenerEventPair), 34 | AddMiddleware(Rc>), 35 | } 36 | 37 | /// A wrapper for an [Rc] reference to a [Store]. 38 | /// 39 | /// This wrapper exists to provide a standard interface for re-useable 40 | /// middleware and other components which may require a long living 41 | /// reference to the store in order to dispatch actions or modify it 42 | /// in some manner that could not be handled by a simple `&Store`. 43 | pub struct StoreRef(Rc>); 44 | 45 | impl StoreRef 46 | where 47 | Event: Clone + Hash + Eq, 48 | { 49 | pub fn new + 'static>( 50 | reducer: R, 51 | initial_state: State, 52 | ) -> Self { 53 | Self(Rc::new(Store::new(reducer, initial_state))) 54 | } 55 | } 56 | 57 | impl Clone for StoreRef { 58 | fn clone(&self) -> Self { 59 | Self(Rc::clone(&self.0)) 60 | } 61 | } 62 | 63 | impl Deref for StoreRef { 64 | type Target = Store; 65 | 66 | fn deref(&self) -> &Self::Target { 67 | &*self.0 68 | } 69 | } 70 | 71 | impl PartialEq for StoreRef { 72 | fn eq(&self, other: &Self) -> bool { 73 | Rc::ptr_eq(&self.0, &other.0) 74 | } 75 | } 76 | 77 | /// This struct is designed to operate as a central source of truth 78 | /// and global "immutable" state within your application. 79 | /// 80 | /// The current state of this store ([Store::state()]()) can only be 81 | /// modified by dispatching an `Action` via [Store::dispatch()] to the 82 | /// store. These actions are taken by a [Reducer] which you provided 83 | /// to the store (at construction) and a new current state is 84 | /// produced. The reducer also produces `Events` associated with the 85 | /// change. The previous state is never mutated, and remains as a 86 | /// reference for any element of your application which may rely upon 87 | /// it (ensure that it gets dropped when it is no longer required, 88 | /// lest it become a memory leak when large `State`s are involved). 89 | /// 90 | /// Listeners can susbscribe to changes to the `State` in this store 91 | /// (and `Event`s produced) with [Store::subscribe()], or they can 92 | /// also subscribe to changes associated with specific `Event`s via 93 | /// [subscribe_event()](Store::subscribe_event())/[subscribe_events()](Store::subscribe_events()). 94 | pub struct Store { 95 | /// This lock is used to prevent dispatch recursion. 96 | dispatch_lock: RefCell<()>, 97 | /// Queue of actions to be dispatched by [Store::dispatch()]. 98 | dispatch_queue: RefCell>, 99 | /// Queue of [StoreModification]s to be executed by 100 | /// [Store::dispatch()] before the next `Action` is dispatched. 101 | modification_queue: RefCell>>, 102 | /// The [Reducer] for this store, which takes `Actions`, modifies 103 | /// the `State` stored in this store, and produces `Events` to be 104 | /// sent to the store listeners. 105 | reducer: Box>, 106 | /// The current state of this store. 107 | state: RefCell>, 108 | /// The listeners which are notified of changes to the state of 109 | /// this store, and events produced by this store during a 110 | /// [Store::dispatch()]. 111 | listeners: RefCell>>, 112 | /// Middleware which modifies the functionality of this store. 113 | #[allow(clippy::type_complexity)] 114 | middleware: RefCell>>>, 115 | /// Used during recursive execution of [Middleware] to keep track 116 | /// of the middleware currently executing. It is an index into 117 | /// [Store::middleware]. 118 | prev_middleware: Cell, 119 | phantom_action: PhantomData, 120 | phantom_event: PhantomData, 121 | } 122 | 123 | impl Store 124 | where 125 | Event: Clone + Hash + Eq, 126 | { 127 | /// Create a new [Store], which uses the specified `reducer` to 128 | /// handle `Action`s which mutate the state and produce `Event`s, 129 | /// and with the `initial_state`. 130 | pub fn new + 'static>( 131 | reducer: R, 132 | initial_state: State, 133 | ) -> Self { 134 | Self { 135 | dispatch_lock: RefCell::new(()), 136 | dispatch_queue: RefCell::new(VecDeque::new()), 137 | modification_queue: RefCell::new(VecDeque::new()), 138 | reducer: Box::new(reducer), 139 | state: RefCell::new(Rc::new(initial_state)), 140 | listeners: RefCell::new(Vec::new()), 141 | middleware: RefCell::new(Vec::new()), 142 | prev_middleware: Cell::new(-1), 143 | phantom_action: PhantomData, 144 | phantom_event: PhantomData, 145 | } 146 | } 147 | 148 | /// Get the current `State` stored in this store. 149 | /// 150 | /// Modifications to this state need to be performed by 151 | /// dispatching an `Action` to the store using 152 | /// [dispatch()](Store::dispatch()). 153 | pub fn state(&self) -> Rc { 154 | self.state.borrow().clone() 155 | } 156 | 157 | /// Dispatch an `Action` to the reducer on this `Store` without 158 | /// invoking middleware. 159 | fn dispatch_reducer(&self, action: &Action) -> ReduceMiddlewareResult { 160 | let result = self.reducer.reduce(&self.state(), action); 161 | *self.state.borrow_mut() = result.state; 162 | 163 | ReduceMiddlewareResult { 164 | events: result.events, 165 | effects: result.effects, 166 | } 167 | } 168 | 169 | /// Dispatch an `Action` to the reducer on this `Store`, invoking 170 | /// all middleware's [reduce()][Middleware::reduce()] first. 171 | fn middleware_reduce(&self, action: &Action) -> ReduceMiddlewareResult { 172 | self.prev_middleware.set(-1); 173 | self.middleware_reduce_next(Some(action)) 174 | } 175 | 176 | /// A recursive function which executes each middleware for this 177 | /// store, and invokes the next middleware, until all middleware 178 | /// has been invoked, at which point the `Action` is sent to the 179 | /// reducer. 180 | fn middleware_reduce_next( 181 | &self, 182 | action: Option<&Action>, 183 | ) -> ReduceMiddlewareResult { 184 | let current_middleware = self.prev_middleware.get() + 1; 185 | self.prev_middleware.set(current_middleware); 186 | 187 | if current_middleware == self.middleware.borrow().len() as i32 { 188 | return match action { 189 | Some(action) => self.dispatch_reducer(action), 190 | None => ReduceMiddlewareResult::default(), 191 | }; 192 | } 193 | 194 | self.middleware.borrow()[current_middleware as usize] 195 | .clone() 196 | .on_reduce(self, action, Self::middleware_reduce_next) 197 | } 198 | 199 | /// Process all the `Effect`s returned by the [Reducer::reduce()] 200 | /// by invoking the middleware on this store to perform the 201 | /// processing using [Middleware::process_effect()].q 202 | fn middleware_process_effects(&self, effects: Vec) { 203 | for effect in effects { 204 | self.middleware_process_effect(effect); 205 | } 206 | } 207 | 208 | /// Process the specified `Effect`, invoking all middleware in this 209 | /// store to perform the processing using 210 | /// [Middleware::process_effect()]. 211 | fn middleware_process_effect(&self, effect: Effect) { 212 | self.prev_middleware.set(-1); 213 | self.middleware_process_effects_next(effect); 214 | } 215 | 216 | /// A recursive function which executes each middleware for this 217 | /// store to process the specified `Effect` with 218 | /// [Middleware::process_effect()], and invokes the next 219 | /// middleware, until all middleware has been invoked. 220 | fn middleware_process_effects_next(&self, effect: Effect) { 221 | let current_middleware = self.prev_middleware.get() + 1; 222 | self.prev_middleware.set(current_middleware); 223 | 224 | if current_middleware == self.middleware.borrow().len() as i32 { 225 | return; 226 | } 227 | 228 | if let Some(effect) = self.middleware.borrow()[current_middleware as usize] 229 | .clone() 230 | .process_effect(self, effect) 231 | { 232 | self.middleware_process_effects_next(effect); 233 | } 234 | } 235 | 236 | /// Notify store listeners of events produced during a reduce as a 237 | /// result of an `Action` being dispatched. Invokes all 238 | /// middleware's [reduce()][Middleware::reduce()] first. 239 | /// Notification occurs even if there are no events to report. 240 | fn middleware_notify(&self, events: Vec) -> Vec { 241 | self.prev_middleware.set(-1); 242 | self.middleware_notify_next(events) 243 | } 244 | 245 | /// A recursive function which executes each middleware for this 246 | /// store, and invokes the next middleware, until all middleware 247 | /// has been invoked, at which point the listeners are notified of 248 | /// the envents produced during a reduce as a result of an 249 | /// `Action` being dispatched. Notification occurs even if there 250 | /// are no events to report. 251 | fn middleware_notify_next(&self, events: Vec) -> Vec { 252 | let current_middleware = self.prev_middleware.get() + 1; 253 | self.prev_middleware.set(current_middleware); 254 | 255 | if current_middleware == self.middleware.borrow().len() as i32 { 256 | return events; 257 | } 258 | 259 | self.middleware.borrow()[current_middleware as usize] 260 | .clone() 261 | .on_notify(self, events, Self::middleware_notify_next) 262 | } 263 | 264 | /// Notify store listeners of events produced during a result of 265 | /// an `Action` being dispatched. Notification occurs even if 266 | /// there are no events to report. 267 | fn notify_listeners(&self, events: Vec) { 268 | let mut listeners_to_remove: Vec = Vec::new(); 269 | for (i, pair) in self.listeners.borrow().iter().enumerate() { 270 | let retain = match pair.listener.as_callback() { 271 | Some(callback) => { 272 | if pair.events.is_empty() { 273 | callback.emit(self.state.borrow().clone(), None); 274 | } else { 275 | // call the listener for every matching listener event 276 | for event in &events { 277 | if pair.events.contains(event) { 278 | callback.emit(self.state.borrow().clone(), Some(event.clone())); 279 | } 280 | } 281 | } 282 | 283 | true 284 | } 285 | None => false, 286 | }; 287 | 288 | if !retain { 289 | listeners_to_remove.insert(0, i); 290 | } 291 | } 292 | 293 | for index in listeners_to_remove { 294 | self.listeners.borrow_mut().swap_remove(index); 295 | } 296 | } 297 | 298 | fn process_pending_modifications(&self) { 299 | while let Some(modification) = self.modification_queue.borrow_mut().pop_front() { 300 | match modification { 301 | StoreModification::AddListener(listener_pair) => { 302 | self.listeners.borrow_mut().push(listener_pair); 303 | } 304 | StoreModification::AddMiddleware(middleware) => { 305 | self.middleware.borrow_mut().push(middleware); 306 | } 307 | } 308 | } 309 | } 310 | 311 | /// Dispatch an `Action` to be passed to the [Reducer] in order to 312 | /// modify the `State` in this store, and produce `Events` to be 313 | /// sent to the store listeners. 314 | pub fn dispatch>(&self, action: A) { 315 | self.dispatch_impl(action.into()); 316 | } 317 | 318 | /// Concrete version of [Store::dispatch()], for code size 319 | /// reduction purposes, to avoid generating multiple versions of 320 | /// this complex function per action that implements 321 | /// `Into`, it is expected that there will be many in a 322 | /// typical application. 323 | fn dispatch_impl(&self, action: Action) { 324 | self.dispatch_queue.borrow_mut().push_back(action); 325 | 326 | // If the lock fails to acquire, then the dispatch is already in progress. 327 | // This prevents recursion, when a listener callback also triggers another 328 | // dispatch. 329 | if let Ok(_lock) = self.dispatch_lock.try_borrow_mut() { 330 | // For some strange reason can't use a while let here because 331 | // it requires Action to implement Copy, and also it was maintaining 332 | // the dispatch_queue borrow during the loop (even though it wasn't needed). 333 | loop { 334 | let dispatch_action = self.dispatch_queue.borrow_mut().pop_front(); 335 | 336 | match dispatch_action { 337 | Some(action) => { 338 | self.process_pending_modifications(); 339 | 340 | let reduce_middleware_result = if self.middleware.borrow().is_empty() { 341 | self.dispatch_reducer(&action) 342 | } else { 343 | self.middleware_reduce(&action) 344 | }; 345 | 346 | #[allow(clippy::match_single_binding)] // destructuring the result 347 | match reduce_middleware_result { 348 | ReduceMiddlewareResult { events, effects } => { 349 | self.middleware_process_effects(effects); 350 | 351 | let middleware_events = self.middleware_notify(events); 352 | if !middleware_events.is_empty() { 353 | self.notify_listeners(middleware_events); 354 | } 355 | } 356 | } 357 | } 358 | None => { 359 | break; 360 | } 361 | } 362 | } 363 | } 364 | } 365 | 366 | /// Subscribe a [Listener] to changes in the store state and 367 | /// events produced by the [Reducer] as a result of `Action`s 368 | /// dispatched via [dispatch()](Store::dispatch()). 369 | /// 370 | /// The listener is a weak reference; when the strong reference 371 | /// associated with it (usually [Callback](crate::Callback)) is 372 | /// dropped, the listener will be removed from this store upon 373 | /// [dispatch()](Store::dispatch()). 374 | /// 375 | /// If you want to subscribe to state changes associated with 376 | /// specific `Event`s, see 377 | /// [subscribe_event()](Store::subscribe_events()) or 378 | /// [subscribe_event()](Store::subscribe_events()) 379 | pub fn subscribe>(&self, listener: L) { 380 | self.modification_queue 381 | .borrow_mut() 382 | .push_back(StoreModification::AddListener(ListenerEventPair { 383 | listener: listener.as_listener(), 384 | events: HashSet::new(), 385 | })); 386 | } 387 | 388 | /// Subscribe a [Listener] to changes in the store state and 389 | /// events produced by the [Reducer] as a result of `Action`s 390 | /// being dispatched via [dispatch()](Store::dispatch()) and 391 | /// reduced with the store's [Reducer]. This subscription is only 392 | /// active changes which produce the specific matching `event` 393 | /// from the [Reducer]. 394 | /// 395 | /// The listener is a weak reference; when the strong reference 396 | /// associated with it (usually [Callback](crate::Callback)) is 397 | /// dropped, the listener will be removed from this store upon 398 | /// [dispatch()](Store::dispatch()). 399 | pub fn subscribe_event>(&self, listener: L, event: Event) { 400 | let mut events = HashSet::with_capacity(1); 401 | events.insert(event); 402 | 403 | self.modification_queue 404 | .borrow_mut() 405 | .push_back(StoreModification::AddListener(ListenerEventPair { 406 | listener: listener.as_listener(), 407 | events, 408 | })); 409 | } 410 | 411 | /// Subscribe a [Listener] to changes in the store state and 412 | /// events produced by the [Reducer] as a result of `Action`s 413 | /// being dispatched via [dispatch()](Store::dispatch()) and 414 | /// reduced with the store's [Reducer]. This subscription is only 415 | /// active changes which produce any of the specific matching 416 | /// `events` from the [Reducer]. 417 | /// 418 | /// The listener is a weak reference; when the strong reference 419 | /// associated with it (usually [Callback](crate::Callback)) is 420 | /// dropped, the listener will be removed from this store upon 421 | /// [dispatch()](Store::dispatch()). 422 | pub fn subscribe_events, E: IntoIterator>( 423 | &self, 424 | listener: L, 425 | events: E, 426 | ) { 427 | self.modification_queue 428 | .borrow_mut() 429 | .push_back(StoreModification::AddListener(ListenerEventPair { 430 | listener: listener.as_listener(), 431 | events: HashSet::from_iter(events.into_iter()), 432 | })); 433 | } 434 | 435 | /// Add [Middleware] to modify the behaviour of this [Store] 436 | /// during a [dispatch()](Store::dispatch()). 437 | pub fn add_middleware + 'static>( 438 | &self, 439 | middleware: M, 440 | ) { 441 | self.modification_queue 442 | .borrow_mut() 443 | .push_back(StoreModification::AddMiddleware(Rc::new(middleware))); 444 | } 445 | } 446 | 447 | #[cfg(test)] 448 | mod tests { 449 | use crate::{ 450 | middleware::{Middleware, ReduceMiddlewareResult}, 451 | Callback, Reducer, ReducerResult, Store, StoreRef, 452 | }; 453 | use std::{cell::RefCell, rc::Rc}; 454 | 455 | #[derive(Debug, PartialEq)] 456 | struct TestState { 457 | counter: i32, 458 | } 459 | 460 | #[derive(Copy, Clone)] 461 | enum TestAction { 462 | Increment, 463 | Decrement, 464 | Decrement2, 465 | Decrent2Then1, 466 | NoEvent, 467 | } 468 | 469 | enum TestEffect { 470 | ChainAction(TestAction), 471 | } 472 | 473 | struct TestReducer; 474 | 475 | impl Reducer for TestReducer { 476 | fn reduce( 477 | &self, 478 | state: &Rc, 479 | action: &TestAction, 480 | ) -> ReducerResult { 481 | let mut events = Vec::new(); 482 | let mut effects = Vec::new(); 483 | 484 | let new_state = match action { 485 | TestAction::Increment => { 486 | events.push(TestEvent::CounterChanged); 487 | TestState { 488 | counter: state.counter + 1, 489 | } 490 | } 491 | TestAction::Decrement => { 492 | events.push(TestEvent::CounterChanged); 493 | TestState { 494 | counter: state.counter - 1, 495 | } 496 | } 497 | TestAction::Decrement2 => { 498 | events.push(TestEvent::CounterChanged); 499 | TestState { 500 | counter: state.counter - 2, 501 | } 502 | } 503 | TestAction::Decrent2Then1 => { 504 | effects.push(TestEffect::ChainAction(TestAction::Decrement)); 505 | events.push(TestEvent::CounterChanged); 506 | 507 | TestState { 508 | counter: state.counter - 2, 509 | } 510 | } 511 | TestAction::NoEvent => TestState { counter: 42 }, 512 | }; 513 | 514 | if new_state.counter != state.counter && new_state.counter == 0 { 515 | events.push(TestEvent::CounterIsZero); 516 | } 517 | 518 | ReducerResult { 519 | state: Rc::new(new_state), 520 | events, 521 | effects, 522 | } 523 | } 524 | } 525 | 526 | struct TestReduceMiddleware { 527 | new_action: TestAction, 528 | } 529 | 530 | impl Middleware for TestReduceMiddleware { 531 | fn on_reduce( 532 | &self, 533 | store: &Store, 534 | action: Option<&TestAction>, 535 | reduce: crate::middleware::ReduceFn, 536 | ) -> ReduceMiddlewareResult { 537 | reduce(store, action.map(|_| &self.new_action)) 538 | } 539 | } 540 | 541 | struct TestEffectMiddleware; 542 | 543 | impl Middleware for TestEffectMiddleware { 544 | fn process_effect( 545 | &self, 546 | store: &Store, 547 | effect: TestEffect, 548 | ) -> Option { 549 | match effect { 550 | TestEffect::ChainAction(action) => { 551 | store.dispatch(action); 552 | } 553 | } 554 | 555 | None 556 | } 557 | } 558 | 559 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 560 | enum TestEvent { 561 | CounterIsZero, 562 | CounterChanged, 563 | } 564 | 565 | #[test] 566 | fn test_notify() { 567 | let initial_state = TestState { counter: 0 }; 568 | let store: Rc>> = 569 | Rc::new(RefCell::new(Store::new(TestReducer, initial_state))); 570 | 571 | let callback_test = Rc::new(RefCell::new(0)); 572 | let callback_test_copy = callback_test.clone(); 573 | let callback: Callback = 574 | Callback::new(move |state: Rc, _| { 575 | *callback_test_copy.borrow_mut() = state.counter; 576 | }); 577 | 578 | store.borrow_mut().subscribe(&callback); 579 | 580 | assert_eq!(0, store.borrow().state().counter); 581 | 582 | store.borrow_mut().dispatch(TestAction::Increment); 583 | store.borrow_mut().dispatch(TestAction::Increment); 584 | assert_eq!(2, *callback_test.borrow()); 585 | assert_eq!(2, store.borrow().state().counter); 586 | 587 | store.borrow_mut().dispatch(TestAction::Decrement); 588 | assert_eq!(1, store.borrow().state().counter); 589 | } 590 | 591 | #[test] 592 | fn test_reduce_middleware() { 593 | let initial_state = TestState { counter: 0 }; 594 | let store = StoreRef::new(TestReducer, initial_state); 595 | 596 | let callback_test = Rc::new(RefCell::new(0)); 597 | let callback_test_copy = callback_test.clone(); 598 | let callback: Callback = 599 | Callback::new(move |state: Rc, _| { 600 | *callback_test_copy.borrow_mut() = state.counter; 601 | }); 602 | 603 | store.subscribe(&callback); 604 | store.add_middleware(TestReduceMiddleware { 605 | new_action: TestAction::Decrement, 606 | }); 607 | store.add_middleware(TestReduceMiddleware { 608 | new_action: TestAction::Decrement2, 609 | }); 610 | 611 | store.dispatch(TestAction::Increment); 612 | assert_eq!(-2, *callback_test.borrow()); 613 | } 614 | 615 | #[test] 616 | fn test_reduce_middleware_reverse_order() { 617 | let initial_state = TestState { counter: 0 }; 618 | let store = StoreRef::new(TestReducer, initial_state); 619 | 620 | let callback_test = Rc::new(RefCell::new(0)); 621 | let callback_test_copy = callback_test.clone(); 622 | let callback: Callback = 623 | Callback::new(move |state: Rc, _| { 624 | *callback_test_copy.borrow_mut() = state.counter; 625 | }); 626 | 627 | store.subscribe(&callback); 628 | store.add_middleware(TestReduceMiddleware { 629 | new_action: TestAction::Decrement2, 630 | }); 631 | store.add_middleware(TestReduceMiddleware { 632 | new_action: TestAction::Decrement, 633 | }); 634 | 635 | store.dispatch(TestAction::Increment); 636 | assert_eq!(-1, *callback_test.borrow()); 637 | } 638 | 639 | #[test] 640 | fn test_effect_middleware() { 641 | let initial_state = TestState { counter: 0 }; 642 | let store = StoreRef::new(TestReducer, initial_state); 643 | store.add_middleware(TestEffectMiddleware); 644 | 645 | assert_eq!(store.state().counter, 0); 646 | store.dispatch(TestAction::Decrent2Then1); 647 | assert_eq!(store.state().counter, -3); 648 | } 649 | 650 | #[test] 651 | fn test_subscribe_event() { 652 | let initial_state = TestState { counter: -2 }; 653 | let store = StoreRef::new(TestReducer, initial_state); 654 | 655 | let callback_test: Rc>> = Rc::new(RefCell::new(None)); 656 | let callback_test_copy = callback_test.clone(); 657 | 658 | let callback_zero_subscription: Callback = 659 | Callback::new(move |_: Rc, event| { 660 | assert_eq!(Some(TestEvent::CounterIsZero), event); 661 | *callback_test_copy.borrow_mut() = Some(TestEvent::CounterIsZero); 662 | }); 663 | 664 | store.subscribe_event(&callback_zero_subscription, TestEvent::CounterIsZero); 665 | store.dispatch(TestAction::Increment); 666 | assert_eq!(None, *callback_test.borrow()); 667 | store.dispatch(TestAction::Increment); 668 | assert_eq!(Some(TestEvent::CounterIsZero), *callback_test.borrow()); 669 | } 670 | 671 | /// Subscribe to an action that produces no events. 672 | #[test] 673 | fn test_subscribe_no_event() { 674 | let initial_state = TestState { counter: 0 }; 675 | let store = StoreRef::new(TestReducer, initial_state); 676 | 677 | let callback_test: Rc> = Rc::new(RefCell::new(0)); 678 | let callback_test_copy = callback_test.clone(); 679 | 680 | let callback: Callback = 681 | Callback::new(move |state: Rc, _event| { 682 | assert_eq!(42, state.counter); 683 | *callback_test_copy.borrow_mut() = state.counter; 684 | }); 685 | 686 | store.subscribe(&callback); 687 | 688 | assert_eq!(0, store.state.borrow().counter); 689 | assert_eq!(0, *callback_test.borrow()); 690 | 691 | store.dispatch(TestAction::NoEvent); 692 | 693 | assert_eq!(42, store.state.borrow().counter); 694 | 695 | // it is expected that the callback will not have been invoked, 696 | // because the action produced no events. 697 | assert_eq!(0, *callback_test.borrow()); 698 | } 699 | } 700 | --------------------------------------------------------------------------------