├── .cargo └── config.toml ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── CLAUDE.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── crates ├── yewdux-input │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── yewdux-macros │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── store.rs ├── yewdux-utils │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── yewdux │ ├── Cargo.toml │ └── src │ ├── anymap.rs │ ├── context.rs │ ├── context_provider.rs │ ├── derived_from.rs │ ├── dispatch.rs │ ├── functional.rs │ ├── lib.rs │ ├── listener.rs │ ├── mrc.rs │ ├── storage.rs │ ├── store.rs │ └── subscriber.rs ├── docs ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ ├── context.md │ ├── default_store.md │ ├── derived_state.md │ ├── dispatch.md │ ├── example.md │ ├── intro.md │ ├── listeners.md │ ├── persistence.md │ ├── reading.md │ ├── setup.md │ ├── ssr.md │ └── store.md └── examples ├── async_proxy ├── Cargo.toml ├── index.html └── src │ ├── main.rs │ └── proxy.rs ├── basic ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── context_root ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── derived_from ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── dynamic-render ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── future ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── history ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── input ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── listener ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── manual ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── multi_component ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── no_copy ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── persistent ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── reducer ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── selector ├── Cargo.toml ├── index.html └── src │ └── main.rs ├── tab_sync ├── Cargo.toml ├── index.html └── src │ └── main.rs └── todomvc ├── Cargo.toml ├── index.html └── src ├── main.rs └── state.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - name: Install mdbook 15 | run: | 16 | mkdir mdbook 17 | curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.14/mdbook-v0.4.14-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook 18 | echo `pwd`/mdbook >> $GITHUB_PATH 19 | - name: Deploy GitHub Pages 20 | run: | 21 | # This assumes your book is in the root of your repository. 22 | # Just add a `cd` here if you need to change to another directory. 23 | cd docs 24 | mdbook build 25 | git worktree add gh-pages 26 | git config user.name "Deploy from CI" 27 | git config user.email "" 28 | cd gh-pages 29 | # Delete the ref to avoid keeping history. 30 | git update-ref -d refs/heads/gh-pages 31 | rm -rf * 32 | mv ../book/* . 33 | git add . 34 | git commit -m "Deploy $GITHUB_SHA to gh-pages" 35 | git push --force --set-upstream origin gh-pages 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | **/*target 3 | **/*dist 4 | .idea 5 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Yewdux Development Guide 2 | 3 | ## Build Commands 4 | - Build all crates: `cargo build` 5 | - Build specific crate: `cargo build -p yewdux` 6 | - Run tests (for non-WASM target): `cargo test --target x86_64-unknown-linux-gnu` 7 | - Run tests without doctests: `cargo test --lib --no-default-features --target x86_64-unknown-linux-gnu` 8 | - Run specific test: `cargo test test_name --target x86_64-unknown-linux-gnu` 9 | - Run example: `cd examples/[example_name] && trunk serve` 10 | - Build documentation: `cd docs && mdbook build` 11 | 12 | ## Code Style 13 | - Use Rust 2021 edition 14 | - Follow standard Rust naming conventions (snake_case for functions, CamelCase for types) 15 | - Format code with `cargo fmt` 16 | - Fix lints with `cargo clippy` 17 | - Derive `Clone`, `PartialEq` for all Store implementations 18 | - Add unit tests for new functionality 19 | - Document public APIs with rustdoc comments 20 | - Use thiserror for error handling 21 | - Keep components small and focused on a single responsibility 22 | - Leverage Yew's function components with hooks pattern 23 | 24 | ## Project Structure 25 | - Core library: `crates/yewdux/src/` 26 | - Macros: `crates/yewdux-macros/src/` 27 | - Examples demonstrate usage patterns in `examples/` -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["crates/*", "examples/*"] 4 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Noah Corona 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Noah Corona 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yewdux 2 | 3 | Ergonomic state management for [Yew](https://yew.rs) applications. 4 | 5 | See the [book](https://intendednull.github.io/yewdux/) for more details. 6 | 7 | ## Example 8 | 9 | ```rust 10 | use yew::prelude::*; 11 | use yewdux::prelude::*; 12 | 13 | #[derive(Default, Clone, PartialEq, Store)] 14 | struct State { 15 | count: u32, 16 | } 17 | 18 | #[function_component] 19 | fn ViewCount() -> Html { 20 | let (state, _) = use_store::(); 21 | html!(state.count) 22 | } 23 | 24 | #[function_component] 25 | fn IncrementCount() -> Html { 26 | let (_, dispatch) = use_store::(); 27 | let onclick = dispatch.reduce_mut_callback(|counter| counter.count += 1); 28 | 29 | html! { 30 | 31 | } 32 | } 33 | 34 | #[function_component] 35 | fn App() -> Html { 36 | html! { 37 | <> 38 | 39 | 40 | 41 | } 42 | } 43 | 44 | fn main() { 45 | yew::Renderer::::new().render(); 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /crates/yewdux-input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yewdux-input" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | yewdux = { path = "../yewdux" } 10 | web-sys = "0.3" 11 | wasm-bindgen = "0.2" 12 | yew = { git = "https://github.com/yewstack/yew.git" } 13 | chrono = "0.4.22" 14 | serde = { version = "1.0.114", features = ["rc"] } 15 | -------------------------------------------------------------------------------- /crates/yewdux-input/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{rc::Rc, str::FromStr}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use wasm_bindgen::JsCast; 5 | use web_sys::{HtmlInputElement, HtmlTextAreaElement}; 6 | use yew::prelude::*; 7 | use yewdux::{prelude::*, Context}; 8 | 9 | pub enum InputElement { 10 | Input(HtmlInputElement), 11 | TextArea(HtmlTextAreaElement), 12 | } 13 | 14 | pub trait FromInputElement: Sized { 15 | fn from_input_element(el: InputElement) -> Option; 16 | } 17 | 18 | impl FromInputElement for T 19 | where 20 | T: FromStr, 21 | { 22 | fn from_input_element(el: InputElement) -> Option { 23 | match el { 24 | InputElement::Input(el) => el.value().parse().ok(), 25 | InputElement::TextArea(el) => el.value().parse().ok(), 26 | } 27 | } 28 | } 29 | 30 | #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] 31 | pub struct Checkbox(bool); 32 | 33 | impl Checkbox { 34 | pub fn checked(&self) -> bool { 35 | self.0 36 | } 37 | } 38 | 39 | impl FromInputElement for Checkbox { 40 | fn from_input_element(el: InputElement) -> Option { 41 | if let InputElement::Input(el) = el { 42 | Some(Self(el.checked())) 43 | } else { 44 | None 45 | } 46 | } 47 | } 48 | 49 | pub trait InputDispatch { 50 | fn context(&self) -> &Context; 51 | 52 | fn input(&self, f: F) -> Callback 53 | where 54 | R: FromInputElement, 55 | F: Fn(Rc, R) -> Rc + 'static, 56 | E: AsRef + JsCast + 'static, 57 | { 58 | let cx = self.context(); 59 | Dispatch::::new(cx).reduce_callback_with(move |s, e| { 60 | if let Some(value) = input_value(e) { 61 | f(s, value) 62 | } else { 63 | s 64 | } 65 | }) 66 | } 67 | 68 | fn input_mut(&self, f: F) -> Callback 69 | where 70 | S: Clone, 71 | R: FromInputElement, 72 | F: Fn(&mut S, R) + 'static, 73 | E: AsRef + JsCast + 'static, 74 | { 75 | let cx = self.context(); 76 | Dispatch::::new(cx).reduce_mut_callback_with(move |s, e| { 77 | if let Some(value) = input_value(e) { 78 | f(s, value); 79 | } 80 | }) 81 | } 82 | } 83 | 84 | impl InputDispatch for Dispatch { 85 | fn context(&self) -> &Context { 86 | self.context() 87 | } 88 | } 89 | 90 | /// Get any parsable value out of an input event. 91 | pub fn input_value(event: E) -> Option 92 | where 93 | R: FromInputElement, 94 | E: AsRef + JsCast, 95 | { 96 | event 97 | .target_dyn_into::() 98 | .and_then(|el| R::from_input_element(InputElement::Input(el))) 99 | .or_else(|| { 100 | event 101 | .target_dyn_into::() 102 | .and_then(|el| R::from_input_element(InputElement::TextArea(el))) 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /crates/yewdux-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yewdux-macros" 3 | version = "0.11.0" 4 | authors = ["Noah "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/yewdux/yewdux" 8 | readme = "../../README.md" 9 | description = "Ergonomic state management for Yew applications" 10 | keywords = ["yew", "state", "redux", "shared", "container"] 11 | categories = ["wasm", "web-programming", "rust-patterns"] 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | darling = "0.20" 18 | proc-macro-error = "1.0.4" 19 | proc-macro2 = "1.0.36" 20 | quote = "1.0.14" 21 | syn = "2" 22 | -------------------------------------------------------------------------------- /crates/yewdux-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro_error::proc_macro_error; 3 | use syn::{parse_macro_input, DeriveInput}; 4 | 5 | mod store; 6 | 7 | #[proc_macro_derive(Store, attributes(store))] 8 | #[proc_macro_error] 9 | pub fn store(input: TokenStream) -> TokenStream { 10 | let input = parse_macro_input!(input as DeriveInput); 11 | store::derive(input).into() 12 | } 13 | -------------------------------------------------------------------------------- /crates/yewdux-macros/src/store.rs: -------------------------------------------------------------------------------- 1 | use darling::{util::PathList, FromDeriveInput}; 2 | use proc_macro2::TokenStream; 3 | use quote::quote; 4 | use syn::DeriveInput; 5 | 6 | #[derive(FromDeriveInput, Default)] 7 | #[darling(default, attributes(store))] 8 | struct Opts { 9 | storage: Option, 10 | storage_tab_sync: bool, 11 | listener: PathList, 12 | derived_from: PathList, 13 | derived_from_mut: PathList, 14 | } 15 | 16 | pub(crate) fn derive(input: DeriveInput) -> TokenStream { 17 | let opts = Opts::from_derive_input(&input).expect("Invalid options"); 18 | let ident = input.ident; 19 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 20 | 21 | let extra_listeners: Vec<_> = opts 22 | .listener 23 | .iter() 24 | .map(|path| { 25 | quote! { 26 | ::yewdux::listener::init_listener( 27 | || #path, cx 28 | ); 29 | } 30 | }) 31 | .collect(); 32 | 33 | let derived_from_init: Vec<_> = opts 34 | .derived_from 35 | .iter() 36 | .map(|source_type| { 37 | quote! { 38 | cx.derived_from::<#source_type, Self>(); 39 | } 40 | }) 41 | .collect(); 42 | 43 | let derived_from_mut_init: Vec<_> = opts 44 | .derived_from_mut 45 | .iter() 46 | .map(|source_type| { 47 | quote! { 48 | cx.derived_from_mut::<#source_type, Self>(); 49 | } 50 | }) 51 | .collect(); 52 | 53 | let impl_ = match opts.storage { 54 | Some(storage) => { 55 | let area = match storage.as_ref() { 56 | "local" => quote! { ::yewdux::storage::Area::Local }, 57 | "session" => quote! { ::yewdux::storage::Area::Session }, 58 | x => panic!( 59 | "'{}' is not a valid option. Must be 'local' or 'session'.", 60 | x 61 | ), 62 | }; 63 | 64 | let sync = if opts.storage_tab_sync { 65 | quote! { 66 | if let Err(err) = ::yewdux::storage::init_tab_sync::(#area, cx) { 67 | ::yewdux::log::error!("Unable to init tab sync for storage: {:?}", err); 68 | } 69 | } 70 | } else { 71 | quote!() 72 | }; 73 | 74 | quote! { 75 | #[cfg(target_arch = "wasm32")] 76 | fn new(cx: &::yewdux::Context) -> Self { 77 | ::yewdux::listener::init_listener( 78 | || ::yewdux::storage::StorageListener::::new(#area), 79 | cx 80 | ); 81 | #(#extra_listeners)* 82 | #(#derived_from_init)* 83 | #(#derived_from_mut_init)* 84 | 85 | #sync 86 | 87 | match ::yewdux::storage::load(#area) { 88 | Ok(val) => val.unwrap_or_default(), 89 | Err(err) => { 90 | ::yewdux::log::error!("Error loading state from storage: {:?}", err); 91 | 92 | Default::default() 93 | } 94 | } 95 | 96 | } 97 | 98 | #[cfg(not(target_arch = "wasm32"))] 99 | fn new(cx: &::yewdux::Context) -> Self { 100 | #(#extra_listeners)* 101 | #(#derived_from_init)* 102 | #(#derived_from_mut_init)* 103 | Default::default() 104 | } 105 | } 106 | } 107 | None => quote! { 108 | fn new(cx: &::yewdux::Context) -> Self { 109 | #(#extra_listeners)* 110 | #(#derived_from_init)* 111 | #(#derived_from_mut_init)* 112 | Default::default() 113 | } 114 | }, 115 | }; 116 | 117 | quote! { 118 | #[automatically_derived] 119 | impl #impl_generics ::yewdux::store::Store for #ident #ty_generics #where_clause { 120 | #impl_ 121 | 122 | fn should_notify(&self, other: &Self) -> bool { 123 | self != other 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /crates/yewdux-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yewdux-utils" 3 | version = "0.11.0" 4 | authors = ["Noah "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/yewdux/yewdux-utils" 8 | readme = "../../README.md" 9 | description = "Ergonomic state management for Yew applications" 10 | keywords = ["yew", "state", "redux", "shared", "container"] 11 | categories = ["wasm", "web-programming", "rust-patterns"] 12 | 13 | 14 | [dependencies] 15 | # yewdux = "0.11.0" 16 | yewdux = { path = "../yewdux" } 17 | 18 | 19 | -------------------------------------------------------------------------------- /crates/yewdux-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, rc::Rc}; 2 | use yewdux::{prelude::*, Context}; 3 | 4 | #[derive(Default)] 5 | pub struct HistoryListener(PhantomData); 6 | 7 | struct HistoryChangeMessage(Rc); 8 | 9 | impl Reducer> for HistoryChangeMessage { 10 | fn apply(self, mut state: Rc>) -> Rc> { 11 | if state.matches_current(&self.0) { 12 | return state; 13 | } 14 | 15 | let mut_state = Rc::make_mut(&mut state); 16 | mut_state.index += 1; 17 | mut_state.vector.truncate(mut_state.index); 18 | mut_state.vector.push(self.0); 19 | 20 | state 21 | } 22 | } 23 | 24 | impl Listener for HistoryListener { 25 | type Store = T; 26 | 27 | fn on_change(&self, cx: &Context, state: Rc) { 28 | Dispatch::>::new(cx).apply(HistoryChangeMessage::(state)) 29 | } 30 | } 31 | 32 | #[derive(Debug, PartialEq)] 33 | pub struct HistoryStore { 34 | vector: Vec>, 35 | index: usize, 36 | dispatch: Dispatch, 37 | } 38 | 39 | impl Clone for HistoryStore { 40 | fn clone(&self) -> Self { 41 | Self { 42 | vector: self.vector.clone(), 43 | index: self.index, 44 | dispatch: self.dispatch.clone(), 45 | } 46 | } 47 | } 48 | 49 | impl HistoryStore { 50 | pub fn can_apply(&self, message: &HistoryMessage) -> bool { 51 | match message { 52 | HistoryMessage::Undo => self.index > 0, 53 | HistoryMessage::Redo => self.index + 1 < self.vector.len(), 54 | HistoryMessage::Clear => self.vector.len() > 1, 55 | HistoryMessage::JumpTo(index) => index != &self.index && index < &self.vector.len(), 56 | } 57 | } 58 | 59 | fn matches_current(&self, state: &Rc) -> bool { 60 | let c = self.current(); 61 | Rc::ptr_eq(c, state) 62 | } 63 | 64 | fn current(&self) -> &Rc { 65 | &self.vector[self.index] 66 | } 67 | 68 | pub fn index(&self) -> usize { 69 | self.index 70 | } 71 | 72 | pub fn states(&self) -> &[Rc] { 73 | self.vector.as_slice() 74 | } 75 | } 76 | 77 | impl Store for HistoryStore { 78 | fn new(cx: &Context) -> Self { 79 | let dispatch = Dispatch::::new(cx); 80 | let s1 = dispatch.get(); 81 | Self { 82 | vector: vec![s1], 83 | index: 0, 84 | dispatch, 85 | } 86 | } 87 | 88 | fn should_notify(&self, other: &Self) -> bool { 89 | self != other 90 | } 91 | } 92 | 93 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 94 | pub enum HistoryMessage { 95 | Undo, 96 | Redo, 97 | Clear, 98 | JumpTo(usize), 99 | } 100 | 101 | impl Reducer> for HistoryMessage { 102 | fn apply(self, mut state: Rc>) -> Rc> { 103 | let mut_state = Rc::make_mut(&mut state); 104 | 105 | let state_changed = match self { 106 | HistoryMessage::Undo => { 107 | if let Some(new_index) = mut_state.index.checked_sub(1) { 108 | mut_state.index = new_index; 109 | true 110 | } else { 111 | false 112 | } 113 | } 114 | HistoryMessage::Redo => { 115 | let new_index = mut_state.index + 1; 116 | if new_index < mut_state.vector.len() { 117 | mut_state.index = new_index; 118 | true 119 | } else { 120 | false 121 | } 122 | } 123 | HistoryMessage::Clear => { 124 | let current = mut_state.vector[mut_state.index].clone(); 125 | mut_state.vector.clear(); 126 | mut_state.vector.push(current); 127 | mut_state.index = 0; 128 | false 129 | } 130 | HistoryMessage::JumpTo(index) => { 131 | if index < mut_state.vector.len() { 132 | mut_state.index = index; 133 | 134 | true 135 | } else { 136 | false 137 | } 138 | } 139 | }; 140 | 141 | if state_changed { 142 | mut_state.dispatch.reduce(|_| mut_state.current().clone()); 143 | } 144 | 145 | state 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /crates/yewdux/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yewdux" 3 | version = "0.11.0" 4 | authors = ["Noah "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/yewdux/yewdux" 8 | readme = "../../README.md" 9 | description = "Ergonomic state management for Yew applications" 10 | keywords = ["yew", "state", "redux", "shared", "container"] 11 | categories = ["wasm", "web-programming", "rust-patterns"] 12 | 13 | [features] 14 | default = ["future"] 15 | future = [] 16 | 17 | # INTERNAL USE ONLY 18 | doctests = [] 19 | 20 | [dependencies] 21 | log = "0.4.16" 22 | serde = { version = "1.0.114", features = ["rc"] } 23 | serde_json = "1.0.64" 24 | slab = "0.4" 25 | thiserror = "1.0" 26 | web-sys = "0.3" 27 | # yew = "0.21" 28 | yew = { git = "https://github.com/yewstack/yew.git" } 29 | # yewdux-macros = "0.11.0" 30 | yewdux-macros = { path = "../yewdux-macros" } 31 | 32 | [target.'cfg(target_arch = "wasm32")'.dependencies] 33 | wasm-bindgen = "0.2" 34 | 35 | 36 | -------------------------------------------------------------------------------- /crates/yewdux/src/anymap.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | collections::HashMap, 4 | }; 5 | 6 | #[derive(Default)] 7 | pub(crate) struct AnyMap { 8 | map: HashMap>, 9 | } 10 | 11 | impl AnyMap { 12 | pub(crate) fn entry(&mut self) -> Entry { 13 | Entry { 14 | map: &mut self.map, 15 | _marker: std::marker::PhantomData, 16 | } 17 | } 18 | } 19 | 20 | pub(crate) struct Entry<'a, T: 'static> { 21 | map: &'a mut HashMap>, 22 | _marker: std::marker::PhantomData, 23 | } 24 | 25 | impl<'a, T: 'static> Entry<'a, T> { 26 | pub(crate) fn or_insert_with(self, default: F) -> &'a mut T 27 | where 28 | F: FnOnce() -> T, 29 | { 30 | let type_id = TypeId::of::(); 31 | let value = self 32 | .map 33 | .entry(type_id) 34 | .or_insert_with(|| Box::new(default())); 35 | value.downcast_mut().expect("type id mismatch") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/yewdux/src/context.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::{ 4 | anymap::AnyMap, 5 | mrc::Mrc, 6 | store::{Reducer, Store}, 7 | subscriber::{Callable, SubscriberId, Subscribers}, 8 | }; 9 | 10 | pub(crate) struct Entry { 11 | pub(crate) store: Mrc>, 12 | } 13 | 14 | impl Clone for Entry { 15 | fn clone(&self) -> Self { 16 | Self { 17 | store: Mrc::clone(&self.store), 18 | } 19 | } 20 | } 21 | 22 | impl Entry { 23 | /// Apply a function to state, returning if it should notify subscribers or not. 24 | pub(crate) fn reduce>(&self, reducer: R) -> bool { 25 | let old = Rc::clone(&self.store.borrow()); 26 | // Apply the reducer. 27 | let new = reducer.apply(Rc::clone(&old)); 28 | // Update to new state. 29 | *self.store.borrow_mut() = new; 30 | // Return whether or not subscribers should be notified. 31 | self.store.borrow().should_notify(&old) 32 | } 33 | } 34 | 35 | /// Execution context for a dispatch 36 | /// 37 | /// # Example 38 | /// 39 | /// ``` 40 | /// use yewdux::prelude::*; 41 | /// 42 | /// #[derive(Clone, PartialEq, Default, Store)] 43 | /// struct Counter(usize); 44 | /// 45 | /// // In a real application, you'd typically get the context from a parent component 46 | /// let cx = yewdux::Context::new(); 47 | /// let dispatch = Dispatch::::new(&cx); 48 | /// ``` 49 | #[derive(Clone, Default, PartialEq)] 50 | pub struct Context { 51 | inner: Mrc, 52 | } 53 | 54 | impl Context { 55 | pub fn new() -> Self { 56 | Default::default() 57 | } 58 | 59 | #[cfg(any(doc, feature = "doctests", target_arch = "wasm32"))] 60 | pub fn global() -> Self { 61 | thread_local! { 62 | static CONTEXT: Context = Default::default(); 63 | } 64 | 65 | CONTEXT 66 | .try_with(|cx| cx.clone()) 67 | .expect("CONTEXTS thread local key init failed") 68 | } 69 | 70 | /// Initialize a store using a custom constructor. `Store::new` will not be called in this 71 | /// case. If already initialized, the custom constructor will not be called. 72 | pub fn init S>(&self, new_store: F) { 73 | self.get_or_init(new_store); 74 | } 75 | 76 | /// Get or initialize a store using a custom constructor. `Store::new` will not be called in 77 | /// this case. If already initialized, the custom constructor will not be called. 78 | pub(crate) fn get_or_init S>(&self, new_store: F) -> Entry { 79 | // Get context, or None if it doesn't exist. 80 | // 81 | // We use an option here because a new Store should not be created during this borrow. We 82 | // want to allow this store access to other stores during creation, so cannot be borrowing 83 | // the global resource while initializing. Instead we create a temporary placeholder, which 84 | // indicates the store needs to be created. Without this indicator we would have needed to 85 | // check if the map contains the entry beforehand, which would have meant two map lookups 86 | // per call instead of just one. 87 | let maybe_entry = self.inner.with_mut(|x| { 88 | x.entry::>>>() 89 | .or_insert_with(|| None.into()) 90 | .clone() 91 | }); 92 | 93 | // If it doesn't exist, create and save the new store. 94 | let exists = maybe_entry.borrow().is_some(); 95 | if !exists { 96 | // Init store outside of borrow. This allows the store to access other stores when it 97 | // is being created. 98 | let entry = Entry { 99 | store: Mrc::new(Rc::new(new_store(self))), 100 | }; 101 | 102 | *maybe_entry.borrow_mut() = Some(entry); 103 | } 104 | 105 | // Now we get the context, which must be initialized because we already checked above. 106 | let entry = maybe_entry 107 | .borrow() 108 | .clone() 109 | .expect("Context not initialized"); 110 | 111 | entry 112 | } 113 | 114 | /// Get or initialize a store with a default Store::new implementation. 115 | pub(crate) fn get_or_init_default(&self) -> Entry { 116 | self.get_or_init(S::new) 117 | } 118 | 119 | pub fn reduce>(&self, r: R) { 120 | let entry = self.get_or_init_default::(); 121 | let should_notify = entry.reduce(r); 122 | 123 | if should_notify { 124 | let state = Rc::clone(&entry.store.borrow()); 125 | self.notify_subscribers(state) 126 | } 127 | } 128 | 129 | pub fn reduce_mut(&self, f: F) { 130 | self.reduce(|mut state| { 131 | f(Rc::make_mut(&mut state)); 132 | state 133 | }); 134 | } 135 | 136 | /// Set state to given value. 137 | pub fn set(&self, value: S) { 138 | self.reduce(move |_| value.into()); 139 | } 140 | 141 | /// Get current state. 142 | pub fn get(&self) -> Rc { 143 | Rc::clone(&self.get_or_init_default::().store.borrow()) 144 | } 145 | 146 | /// Send state to all subscribers. 147 | pub fn notify_subscribers(&self, state: Rc) { 148 | let entry = self.get_or_init_default::>>(); 149 | entry.store.borrow().notify(state); 150 | } 151 | 152 | /// Subscribe to a store. `on_change` is called immediately, then every time state changes. 153 | pub fn subscribe>(&self, on_change: N) -> SubscriberId { 154 | // Notify subscriber with inital state. 155 | on_change.call(self.get::()); 156 | 157 | self.get_or_init_default::>>() 158 | .store 159 | .borrow() 160 | .subscribe(on_change) 161 | } 162 | 163 | /// Similar to [Self::subscribe], however state is not called immediately. 164 | pub fn subscribe_silent>(&self, on_change: N) -> SubscriberId { 165 | self.get_or_init_default::>>() 166 | .store 167 | .borrow() 168 | .subscribe(on_change) 169 | } 170 | 171 | /// Initialize a listener 172 | pub fn init_listener L>(&self, new_listener: F) { 173 | crate::init_listener(new_listener, self); 174 | } 175 | 176 | pub fn derived_from(&self) 177 | where 178 | Store: crate::Store, 179 | Derived: crate::derived_from::DerivedFrom, 180 | { 181 | crate::derived_from::derive_from::(self); 182 | } 183 | 184 | pub fn derived_from_mut(&self) 185 | where 186 | Store: crate::Store, 187 | Derived: crate::derived_from::DerivedFromMut, 188 | { 189 | crate::derived_from::derive_from_mut::(self); 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use std::cell::Cell; 196 | 197 | use super::*; 198 | 199 | #[derive(Clone, PartialEq, Eq)] 200 | struct TestState(u32); 201 | impl Store for TestState { 202 | fn new(_cx: &Context) -> Self { 203 | Self(0) 204 | } 205 | 206 | fn should_notify(&self, other: &Self) -> bool { 207 | self != other 208 | } 209 | } 210 | 211 | #[derive(Clone, PartialEq, Eq)] 212 | struct TestState2(u32); 213 | impl Store for TestState2 { 214 | fn new(cx: &Context) -> Self { 215 | cx.get_or_init_default::(); 216 | Self(0) 217 | } 218 | 219 | fn should_notify(&self, other: &Self) -> bool { 220 | self != other 221 | } 222 | } 223 | 224 | #[test] 225 | fn can_access_other_store_for_new_of_current_store() { 226 | let _context = Context::new().get_or_init_default::(); 227 | } 228 | 229 | #[derive(Clone, PartialEq, Eq)] 230 | struct StoreNewIsOnlyCalledOnce(Rc>); 231 | impl Store for StoreNewIsOnlyCalledOnce { 232 | fn new(_cx: &Context) -> Self { 233 | thread_local! { 234 | /// Stores all shared state. 235 | static COUNT: Rc> = Default::default(); 236 | } 237 | 238 | let count = COUNT.try_with(|x| x.clone()).unwrap(); 239 | 240 | count.set(count.get() + 1); 241 | 242 | Self(count) 243 | } 244 | 245 | fn should_notify(&self, other: &Self) -> bool { 246 | self != other 247 | } 248 | } 249 | 250 | #[test] 251 | fn store_new_is_only_called_once() { 252 | let cx = Context::new(); 253 | cx.get_or_init_default::(); 254 | let entry = cx.get_or_init_default::(); 255 | 256 | assert!(entry.store.borrow().0.get() == 1) 257 | } 258 | 259 | #[test] 260 | fn recursive_reduce() { 261 | let cx = Context::new(); 262 | let cx2 = cx.clone(); 263 | cx.reduce::(|_s: Rc| { 264 | cx2.reduce::(|s: Rc| TestState(s.0 + 1).into()); 265 | TestState(cx2.get::().0 + 1).into() 266 | }); 267 | 268 | assert_eq!(cx.get::().0, 2); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /crates/yewdux/src/context_provider.rs: -------------------------------------------------------------------------------- 1 | use yew::prelude::*; 2 | 3 | use crate::context; 4 | 5 | #[derive(PartialEq, Clone, Properties)] 6 | pub struct Props { 7 | pub children: Children, 8 | } 9 | 10 | #[function_component] 11 | pub fn YewduxRoot(Props { children }: &Props) -> Html { 12 | let ctx = use_state(context::Context::new); 13 | html! { 14 | context={(*ctx).clone()}> 15 | { children.clone() } 16 | > 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/yewdux/src/derived_from.rs: -------------------------------------------------------------------------------- 1 | //! Provides functionality for creating derived stores that automatically update 2 | //! based on changes to other stores. 3 | //! 4 | //! This module enables the creation of stores that are computed from other stores, 5 | //! allowing for automatic synchronization when the source stores change. 6 | //! 7 | //! There are two approaches available: 8 | //! - `DerivedFrom`: For immutable transformations where a new derived store is created on each update 9 | //! - `DerivedFromMut`: For mutable transformations where the derived store is updated in-place 10 | 11 | use std::rc::Rc; 12 | 13 | use crate::Context; 14 | 15 | /// Trait for creating a derived store that transforms from another store immutably. 16 | /// 17 | /// Implementors of this trait represent a store that derives its state from another store. 18 | /// When the source store changes, `on_change` is called to create a new instance of the derived store. 19 | /// 20 | /// # Type Parameters 21 | /// 22 | /// * `Store`: The source store type this store derives from 23 | pub trait DerivedFrom: crate::Store + 'static { 24 | /// Creates a new instance of the derived store based on the current state of the source store. 25 | /// 26 | /// # Parameters 27 | /// 28 | /// * `state`: The current state of the source store 29 | /// 30 | /// # Returns 31 | /// 32 | /// A new instance of the derived store 33 | fn on_change(&self, state: Rc) -> Self; 34 | } 35 | 36 | /// Internal listener that updates the derived store when the source store changes. 37 | /// 38 | /// This struct implements the `Listener` trait for the source store and manages 39 | /// updating the derived store through its `Dispatch`. 40 | struct Listener 41 | where 42 | Store: crate::Store, 43 | Derived: DerivedFrom, 44 | { 45 | derived: crate::Dispatch, 46 | _marker: std::marker::PhantomData, 47 | } 48 | 49 | impl crate::Listener for Listener 50 | where 51 | Store: crate::Store, 52 | Derived: DerivedFrom, 53 | { 54 | type Store = Store; 55 | 56 | fn on_change(&self, _cx: &Context, state: Rc) { 57 | self.derived 58 | .reduce(|derived| derived.on_change(Rc::clone(&state)).into()); 59 | } 60 | } 61 | 62 | /// Initializes a derived store that automatically updates when the source store changes. 63 | /// 64 | /// This function sets up a listener on the source store that will update the derived store 65 | /// whenever the source store changes, using the `DerivedFrom` implementation to transform the state. 66 | /// 67 | /// # Type Parameters 68 | /// 69 | /// * `Store`: The source store type to derive from 70 | /// * `Derived`: The derived store type that implements `DerivedFrom` 71 | /// 72 | /// # Parameters 73 | /// 74 | /// * `cx`: The Yewdux context 75 | /// 76 | /// # Example 77 | /// 78 | /// ```rust 79 | /// use std::rc::Rc; 80 | /// use yewdux::{Context, Store, Dispatch}; 81 | /// use yewdux::derived_from::{DerivedFrom, derive_from}; 82 | /// 83 | /// #[derive(Clone, PartialEq)] 84 | /// struct SourceStore { value: i32 } 85 | /// impl Store for SourceStore { 86 | /// fn new(_: &Context) -> Self { Self { value: 0 } } 87 | /// fn should_notify(&self, old: &Self) -> bool { self != old } 88 | /// } 89 | /// 90 | /// #[derive(Clone, PartialEq)] 91 | /// struct DerivedStore { doubled_value: i32 } 92 | /// impl Store for DerivedStore { 93 | /// fn new(_: &Context) -> Self { Self { doubled_value: 0 } } 94 | /// fn should_notify(&self, old: &Self) -> bool { self != old } 95 | /// } 96 | /// 97 | /// impl DerivedFrom for DerivedStore { 98 | /// fn on_change(&self, source: Rc) -> Self { 99 | /// Self { doubled_value: source.value * 2 } 100 | /// } 101 | /// } 102 | /// 103 | /// // Create a context - in a real application, you'd typically get this from a parent component 104 | /// let cx = Context::new(); 105 | /// 106 | /// // Set up the derived relationship 107 | /// derive_from::(&cx); 108 | /// 109 | /// // Get dispatches for both stores 110 | /// let source_dispatch = Dispatch::::new(&cx); 111 | /// let derived_dispatch = Dispatch::::new(&cx); 112 | /// 113 | /// source_dispatch.reduce_mut(|state| state.value = 5); 114 | /// assert_eq!(derived_dispatch.get().doubled_value, 10); 115 | /// ``` 116 | pub fn derive_from(cx: &Context) 117 | where 118 | Store: crate::Store, 119 | Derived: DerivedFrom, 120 | { 121 | crate::init_listener( 122 | || Listener { 123 | derived: crate::Dispatch::::new(cx), 124 | _marker: std::marker::PhantomData, 125 | }, 126 | cx, 127 | ); 128 | } 129 | 130 | /// Trait for creating a derived store that is mutably updated from another store. 131 | /// 132 | /// Implementors of this trait represent a store that derives its state from another store. 133 | /// When the source store changes, `on_change` is called to mutably update the derived store. 134 | /// 135 | /// # Type Parameters 136 | /// 137 | /// * `Store`: The source store type this store derives from 138 | pub trait DerivedFromMut: crate::Store + Clone + 'static { 139 | /// Updates the derived store based on the current state of the source store. 140 | /// 141 | /// # Parameters 142 | /// 143 | /// * `state`: The current state of the source store 144 | fn on_change(&mut self, state: Rc); 145 | } 146 | 147 | /// Internal listener that mutably updates the derived store when the source store changes. 148 | /// 149 | /// This struct implements the `Listener` trait for the source store and manages 150 | /// updating the derived store through its `Dispatch` using mutable references. 151 | struct ListenerMut 152 | where 153 | Store: crate::Store, 154 | Derived: DerivedFromMut, 155 | { 156 | derived: crate::Dispatch, 157 | _marker: std::marker::PhantomData, 158 | } 159 | 160 | impl crate::Listener for ListenerMut 161 | where 162 | Store: crate::Store, 163 | Derived: DerivedFromMut, 164 | { 165 | type Store = Store; 166 | 167 | fn on_change(&self, _cx: &Context, state: Rc) { 168 | self.derived 169 | .reduce_mut(|derived| derived.on_change(Rc::clone(&state))); 170 | } 171 | } 172 | 173 | /// Initializes a derived store that is mutably updated when the source store changes. 174 | /// 175 | /// This function sets up a listener on the source store that will update the derived store 176 | /// whenever the source store changes, using the `DerivedFromMut` implementation to transform the state. 177 | /// 178 | /// # Type Parameters 179 | /// 180 | /// * `Store`: The source store type to derive from 181 | /// * `Derived`: The derived store type that implements `DerivedFromMut` 182 | /// 183 | /// # Parameters 184 | /// 185 | /// * `cx`: The Yewdux context 186 | /// 187 | /// # Example 188 | /// 189 | /// ```rust 190 | /// use std::rc::Rc; 191 | /// use yewdux::{Context, Store, Dispatch}; 192 | /// use yewdux::derived_from::{DerivedFromMut, derive_from_mut}; 193 | /// 194 | /// #[derive(Clone, PartialEq)] 195 | /// struct SourceStore { value: i32 } 196 | /// impl Store for SourceStore { 197 | /// fn new(_: &Context) -> Self { Self { value: 0 } } 198 | /// fn should_notify(&self, old: &Self) -> bool { self != old } 199 | /// } 200 | /// 201 | /// #[derive(Clone, PartialEq)] 202 | /// struct DerivedStore { doubled_value: i32 } 203 | /// impl Store for DerivedStore { 204 | /// fn new(_: &Context) -> Self { Self { doubled_value: 0 } } 205 | /// fn should_notify(&self, old: &Self) -> bool { self != old } 206 | /// } 207 | /// 208 | /// impl DerivedFromMut for DerivedStore { 209 | /// fn on_change(&mut self, source: Rc) { 210 | /// self.doubled_value = source.value * 2; 211 | /// } 212 | /// } 213 | /// 214 | /// // Create a context - in a real application, you'd typically get this from a parent component 215 | /// let cx = Context::new(); 216 | /// 217 | /// // Set up the derived relationship with mutable updates 218 | /// derive_from_mut::(&cx); 219 | /// 220 | /// // Get dispatches for both stores 221 | /// let source_dispatch = Dispatch::::new(&cx); 222 | /// let derived_dispatch = Dispatch::::new(&cx); 223 | /// 224 | /// source_dispatch.reduce_mut(|state| state.value = 5); 225 | /// assert_eq!(derived_dispatch.get().doubled_value, 10); 226 | /// ``` 227 | pub fn derive_from_mut(cx: &Context) 228 | where 229 | Store: crate::Store, 230 | Derived: DerivedFromMut, 231 | { 232 | crate::init_listener( 233 | || ListenerMut { 234 | derived: crate::Dispatch::::new(cx), 235 | _marker: std::marker::PhantomData, 236 | }, 237 | cx, 238 | ); 239 | } 240 | 241 | #[cfg(test)] 242 | mod tests { 243 | use crate::Dispatch; 244 | 245 | use super::*; 246 | 247 | #[test] 248 | fn can_derive_from() { 249 | #[derive(Clone, PartialEq, Eq)] 250 | struct TestState(u32); 251 | impl crate::Store for TestState { 252 | fn new(_cx: &crate::Context) -> Self { 253 | Self(0) 254 | } 255 | 256 | fn should_notify(&self, other: &Self) -> bool { 257 | self != other 258 | } 259 | } 260 | 261 | #[derive(Clone, PartialEq, Eq)] 262 | struct TestDerived(u32); 263 | impl crate::Store for TestDerived { 264 | fn new(_cx: &crate::Context) -> Self { 265 | Self(0) 266 | } 267 | 268 | fn should_notify(&self, other: &Self) -> bool { 269 | self != other 270 | } 271 | } 272 | 273 | impl DerivedFrom for TestDerived { 274 | fn on_change(&self, state: Rc) -> Self { 275 | Self(state.0) 276 | } 277 | } 278 | 279 | let cx = crate::Context::new(); 280 | derive_from::(&cx); 281 | 282 | let dispatch_derived = Dispatch::::new(&cx); 283 | let dispatch_state = Dispatch::::new(&cx); 284 | 285 | dispatch_state.reduce_mut(|state| state.0 += 1); 286 | assert_eq!(dispatch_derived.get().0, 1); 287 | } 288 | 289 | #[test] 290 | fn can_derive_from_mut() { 291 | #[derive(Clone, PartialEq, Eq)] 292 | struct TestState(u32); 293 | impl crate::Store for TestState { 294 | fn new(_cx: &crate::Context) -> Self { 295 | Self(0) 296 | } 297 | 298 | fn should_notify(&self, other: &Self) -> bool { 299 | self != other 300 | } 301 | } 302 | 303 | #[derive(Clone, PartialEq, Eq)] 304 | struct TestDerived(u32); 305 | impl crate::Store for TestDerived { 306 | fn new(_cx: &crate::Context) -> Self { 307 | Self(0) 308 | } 309 | 310 | fn should_notify(&self, other: &Self) -> bool { 311 | self != other 312 | } 313 | } 314 | 315 | impl DerivedFromMut for TestDerived { 316 | fn on_change(&mut self, state: Rc) { 317 | self.0 = state.0; 318 | } 319 | } 320 | 321 | let cx = crate::Context::new(); 322 | derive_from_mut::(&cx); 323 | 324 | let dispatch_derived = Dispatch::::new(&cx); 325 | let dispatch_state = Dispatch::::new(&cx); 326 | 327 | dispatch_state.reduce_mut(|state| state.0 += 1); 328 | assert_eq!(dispatch_derived.get().0, 1); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /crates/yewdux/src/dispatch.rs: -------------------------------------------------------------------------------- 1 | //! This module defines how you can interact with your [`Store`]. 2 | //! 3 | //! ``` 4 | //! use yewdux::prelude::*; 5 | //! 6 | //! #[derive(Default, Clone, PartialEq, Store)] 7 | //! struct State { 8 | //! count: usize, 9 | //! } 10 | //! 11 | //! // Create a context - in a real application, you'd typically get this from a parent component 12 | //! let cx = yewdux::Context::new(); 13 | //! let dispatch = Dispatch::::new(&cx); 14 | //! 15 | //! // Update the state 16 | //! dispatch.reduce_mut(|state| state.count = 1); 17 | //! 18 | //! // Get the current state 19 | //! let state = dispatch.get(); 20 | //! 21 | //! assert!(state.count == 1); 22 | //! ``` 23 | //! 24 | //! ## Usage with YewduxRoot 25 | //! 26 | //! For applications with server-side rendering (SSR) support, the recommended 27 | //! approach is to use `YewduxRoot` to provide context: 28 | //! 29 | //! ``` 30 | //! use std::rc::Rc; 31 | //! use yew::prelude::*; 32 | //! use yewdux::prelude::*; 33 | //! 34 | //! // Define your store 35 | //! #[derive(Default, Clone, PartialEq, Store)] 36 | //! struct State { 37 | //! count: u32, 38 | //! } 39 | //! 40 | //! // Function component using hooks to access state 41 | //! #[function_component] 42 | //! fn Counter() -> Html { 43 | //! // Get both state and dispatch from the context 44 | //! let (state, dispatch) = use_store::(); 45 | //! let onclick = dispatch.reduce_mut_callback(|state| state.count += 1); 46 | //! 47 | //! html! { 48 | //! <> 49 | //!

{ state.count }

50 | //! 51 | //! 52 | //! } 53 | //! } 54 | //! 55 | //! // Root component that sets up the YewduxRoot context 56 | //! #[function_component] 57 | //! fn App() -> Html { 58 | //! html! { 59 | //! 60 | //! 61 | //! 62 | //! } 63 | //! } 64 | //! ``` 65 | //! 66 | 67 | use std::{future::Future, rc::Rc}; 68 | 69 | use yew::Callback; 70 | 71 | use crate::{ 72 | context::Context, 73 | store::{Reducer, Store}, 74 | subscriber::{Callable, SubscriberId}, 75 | }; 76 | 77 | /// The primary interface to a [`Store`]. 78 | pub struct Dispatch { 79 | pub(crate) _subscriber_id: Option>>, 80 | pub(crate) cx: Context, 81 | } 82 | 83 | impl std::fmt::Debug for Dispatch { 84 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 85 | f.debug_struct("Dispatch") 86 | .field("_subscriber_id", &self._subscriber_id) 87 | .finish() 88 | } 89 | } 90 | 91 | #[cfg(any(doc, feature = "doctests", target_arch = "wasm32"))] 92 | impl Default for Dispatch { 93 | fn default() -> Self { 94 | Self::global() 95 | } 96 | } 97 | 98 | impl Dispatch { 99 | /// Create a new dispatch with the global context (thread local). 100 | /// 101 | /// This is only available for wasm. For SSR, see the YewduxRoot pattern. 102 | #[cfg(any(doc, feature = "doctests", target_arch = "wasm32"))] 103 | pub fn global() -> Self { 104 | Self::new(&Context::global()) 105 | } 106 | 107 | /// Create a new dispatch with access to the given context. 108 | pub fn new(cx: &Context) -> Self { 109 | Self { 110 | _subscriber_id: Default::default(), 111 | cx: cx.clone(), 112 | } 113 | } 114 | 115 | /// Get the context used by this dispatch. 116 | pub fn context(&self) -> &Context { 117 | &self.cx 118 | } 119 | 120 | /// Spawn a future with access to this dispatch. 121 | #[cfg(feature = "future")] 122 | pub fn spawn_future(&self, f: F) 123 | where 124 | F: FnOnce(Self) -> FU, 125 | FU: Future + 'static, 126 | { 127 | yew::platform::spawn_local(f(self.clone())); 128 | } 129 | 130 | /// Create a callback that will spawn a future with access to this dispatch. 131 | #[cfg(feature = "future")] 132 | pub fn future_callback(&self, f: F) -> Callback 133 | where 134 | F: Fn(Self) -> FU + 'static, 135 | FU: Future + 'static, 136 | { 137 | let dispatch = self.clone(); 138 | Callback::from(move |_| dispatch.spawn_future(&f)) 139 | } 140 | 141 | /// Create a callback that will spawn a future with access to this dispatch and the emitted 142 | /// event. 143 | #[cfg(feature = "future")] 144 | pub fn future_callback_with(&self, f: F) -> Callback 145 | where 146 | F: Fn(Self, E) -> FU + 'static, 147 | FU: Future + 'static, 148 | { 149 | let dispatch = self.clone(); 150 | Callback::from(move |e| dispatch.spawn_future(|dispatch| f(dispatch, e))) 151 | } 152 | 153 | /// Create a dispatch that subscribes to changes in state. Latest state is sent immediately, 154 | /// and on every subsequent change. Automatically unsubscribes when this dispatch is dropped. 155 | /// 156 | /// ## Higher-Order Component Pattern with YewduxRoot 157 | /// 158 | /// ``` 159 | /// use std::rc::Rc; 160 | /// 161 | /// use yew::prelude::*; 162 | /// use yewdux::prelude::*; 163 | /// 164 | /// #[derive(Default, Clone, PartialEq, Eq, Store)] 165 | /// struct State { 166 | /// count: u32, 167 | /// } 168 | /// 169 | /// // Props for our struct component 170 | /// #[derive(Properties, PartialEq, Clone)] 171 | /// struct CounterProps { 172 | /// dispatch: Dispatch, 173 | /// } 174 | /// 175 | /// // Message type for state updates 176 | /// enum Msg { 177 | /// StateChanged(Rc), 178 | /// } 179 | /// 180 | /// // Our struct component that uses the state 181 | /// struct Counter { 182 | /// state: Rc, 183 | /// dispatch: Dispatch, 184 | /// } 185 | /// 186 | /// impl Component for Counter { 187 | /// type Message = Msg; 188 | /// type Properties = CounterProps; 189 | /// 190 | /// fn create(ctx: &Context) -> Self { 191 | /// // Subscribe to state changes 192 | /// let callback = ctx.link().callback(Msg::StateChanged); 193 | /// let dispatch = ctx.props().dispatch.clone().subscribe_silent(callback); 194 | /// 195 | /// Self { 196 | /// state: dispatch.get(), 197 | /// dispatch, 198 | /// } 199 | /// } 200 | /// 201 | /// fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { 202 | /// match msg { 203 | /// Msg::StateChanged(state) => { 204 | /// self.state = state; 205 | /// true 206 | /// } 207 | /// } 208 | /// } 209 | /// 210 | /// fn view(&self, _ctx: &Context) -> Html { 211 | /// let count = self.state.count; 212 | /// let onclick = self.dispatch.reduce_mut_callback(|s| s.count += 1); 213 | /// 214 | /// html! { 215 | /// <> 216 | ///

{ count }

217 | /// 218 | /// 219 | /// } 220 | /// } 221 | /// } 222 | /// 223 | /// // Higher-Order Component (HOC) that accesses the context 224 | /// #[function_component] 225 | /// fn CounterHoc() -> Html { 226 | /// // Use the hook to get the dispatch from context 227 | /// let dispatch = use_dispatch::(); 228 | /// 229 | /// html! { 230 | /// 231 | /// } 232 | /// } 233 | /// 234 | /// // App component with YewduxRoot for SSR support 235 | /// #[function_component] 236 | /// fn App() -> Html { 237 | /// html! { 238 | /// 239 | /// 240 | /// 241 | /// } 242 | /// } 243 | /// ``` 244 | pub fn subscribe>(self, on_change: C) -> Self { 245 | let id = self.cx.subscribe(on_change); 246 | 247 | Self { 248 | _subscriber_id: Some(Rc::new(id)), 249 | cx: self.cx, 250 | } 251 | } 252 | 253 | /// Create a dispatch that subscribes to changes in state. Similar to [Self::subscribe], 254 | /// however state is **not** sent immediately. Automatically unsubscribes when this dispatch is 255 | /// dropped. 256 | pub fn subscribe_silent>(self, on_change: C) -> Self { 257 | let id = self.cx.subscribe_silent(on_change); 258 | 259 | Self { 260 | _subscriber_id: Some(Rc::new(id)), 261 | cx: self.cx, 262 | } 263 | } 264 | 265 | /// Get the current state. 266 | pub fn get(&self) -> Rc { 267 | self.cx.get::() 268 | } 269 | 270 | /// Apply a [`Reducer`](crate::store::Reducer) immediately. 271 | /// 272 | /// ``` 273 | /// # use std::rc::Rc; 274 | /// # use yew::prelude::*; 275 | /// # use yewdux::prelude::*; 276 | /// #[derive(Default, Clone, PartialEq, Eq, Store)] 277 | /// struct State { 278 | /// count: u32, 279 | /// } 280 | /// 281 | /// struct AddOne; 282 | /// impl Reducer for AddOne { 283 | /// fn apply(self, state: Rc) -> Rc { 284 | /// State { 285 | /// count: state.count + 1, 286 | /// } 287 | /// .into() 288 | /// } 289 | /// } 290 | /// 291 | /// # fn main() { 292 | /// # // Context handling code is omitted for clarity 293 | /// # let cx = yewdux::Context::new(); 294 | /// # let dispatch = Dispatch::::new(&cx); 295 | /// // Apply a reducer to update the state 296 | /// dispatch.apply(AddOne); 297 | /// # ; 298 | /// # } 299 | /// ``` 300 | pub fn apply>(&self, reducer: R) { 301 | self.cx.reduce(reducer); 302 | } 303 | 304 | /// Create a callback that applies a [`Reducer`](crate::store::Reducer). 305 | /// 306 | /// ``` 307 | /// # use std::rc::Rc; 308 | /// # use yew::prelude::*; 309 | /// # use yewdux::prelude::*; 310 | /// #[derive(Default, Clone, PartialEq, Eq, Store)] 311 | /// struct State { 312 | /// count: u32, 313 | /// } 314 | /// 315 | /// struct AddOne; 316 | /// impl Reducer for AddOne { 317 | /// fn apply(self, state: Rc) -> Rc { 318 | /// State { 319 | /// count: state.count + 1, 320 | /// } 321 | /// .into() 322 | /// } 323 | /// } 324 | /// 325 | /// # fn main() { 326 | /// # // Context handling code is omitted for clarity 327 | /// # let cx = yewdux::Context::new(); 328 | /// # let dispatch = Dispatch::::new(&cx); 329 | /// // Create a callback that will update the state when triggered 330 | /// let onclick = dispatch.apply_callback(|_| AddOne); 331 | /// html! { 332 | /// 333 | /// } 334 | /// # ; 335 | /// # } 336 | /// ``` 337 | pub fn apply_callback(&self, f: F) -> Callback 338 | where 339 | M: Reducer, 340 | F: Fn(E) -> M + 'static, 341 | { 342 | let context = self.cx.clone(); 343 | Callback::from(move |e| { 344 | let msg = f(e); 345 | context.reduce(msg); 346 | }) 347 | } 348 | 349 | /// Set state to given value immediately. 350 | /// 351 | /// ``` 352 | /// # use yew::prelude::*; 353 | /// # use yewdux::prelude::*; 354 | /// # #[derive(Default, Clone, PartialEq, Eq, Store)] 355 | /// # struct State { 356 | /// # count: u32, 357 | /// # } 358 | /// # fn main() { 359 | /// # // Context handling code is omitted for clarity 360 | /// # let cx = yewdux::Context::new(); 361 | /// # let dispatch = Dispatch::::new(&cx); 362 | /// // Set the state to a new value 363 | /// dispatch.set(State { count: 0 }); 364 | /// # } 365 | /// ``` 366 | pub fn set(&self, val: S) { 367 | self.cx.set(val); 368 | } 369 | 370 | /// Set state using value from callback. 371 | /// 372 | /// ``` 373 | /// # use yew::prelude::*; 374 | /// # use yewdux::prelude::*; 375 | /// # #[derive(Default, Clone, PartialEq, Eq, Store)] 376 | /// # struct State { 377 | /// # count: u32, 378 | /// # } 379 | /// # #[hook] 380 | /// # fn use_foo() { 381 | /// let dispatch = use_dispatch::(); 382 | /// let onchange = dispatch.set_callback(|event: Event| { 383 | /// let value = event.target_unchecked_into::().value(); 384 | /// State { count: value.parse().unwrap() } 385 | /// }); 386 | /// html! { 387 | /// 388 | /// } 389 | /// # ; 390 | /// # } 391 | /// ``` 392 | pub fn set_callback(&self, f: F) -> Callback 393 | where 394 | F: Fn(E) -> S + 'static, 395 | { 396 | let context = self.cx.clone(); 397 | Callback::from(move |e| { 398 | let val = f(e); 399 | context.set(val); 400 | }) 401 | } 402 | 403 | /// Change state immediately. 404 | /// 405 | /// ``` 406 | /// # use yew::prelude::*; 407 | /// # use yewdux::prelude::*; 408 | /// # #[derive(Default, Clone, PartialEq, Eq, Store)] 409 | /// # struct State { 410 | /// # count: u32, 411 | /// # } 412 | /// # fn main() { 413 | /// # // Context handling code is omitted for clarity 414 | /// # let cx = yewdux::Context::new(); 415 | /// # let dispatch = Dispatch::::new(&cx); 416 | /// // Transform the current state into a new state 417 | /// dispatch.reduce(|state| State { count: state.count + 1 }.into()); 418 | /// # } 419 | /// ``` 420 | pub fn reduce(&self, f: F) 421 | where 422 | F: FnOnce(Rc) -> Rc, 423 | { 424 | self.cx.reduce(f); 425 | } 426 | 427 | /// Create a callback that changes state. 428 | /// 429 | /// ``` 430 | /// # use yew::prelude::*; 431 | /// # use yewdux::prelude::*; 432 | /// # #[derive(Default, Clone, PartialEq, Eq, Store)] 433 | /// # struct State { 434 | /// # count: u32, 435 | /// # } 436 | /// # fn main() { 437 | /// # // Context handling code is omitted for clarity 438 | /// # let cx = yewdux::Context::new(); 439 | /// # let dispatch = Dispatch::::new(&cx); 440 | /// // Create a callback that will transform the state when triggered 441 | /// let onclick = dispatch.reduce_callback(|state| State { count: state.count + 1 }.into()); 442 | /// html! { 443 | /// 444 | /// } 445 | /// # ; 446 | /// # } 447 | /// ``` 448 | pub fn reduce_callback(&self, f: F) -> Callback 449 | where 450 | F: Fn(Rc) -> Rc + 'static, 451 | E: 'static, 452 | { 453 | let context = self.cx.clone(); 454 | Callback::from(move |_| { 455 | context.reduce(&f); 456 | }) 457 | } 458 | 459 | /// Similar to [Self::reduce_callback] but also provides the fired event. 460 | /// 461 | /// ``` 462 | /// # use yew::prelude::*; 463 | /// # use yewdux::prelude::*; 464 | /// # #[derive(Default, Clone, PartialEq, Eq, Store)] 465 | /// # struct State { 466 | /// # count: u32, 467 | /// # } 468 | /// # fn main() { 469 | /// # // Context handling code is omitted for clarity 470 | /// # let cx = yewdux::Context::new(); 471 | /// # let dispatch = Dispatch::::new(&cx); 472 | /// // Create a callback that will transform the state using the event data 473 | /// let onchange = dispatch.reduce_callback_with(|state, event: Event| { 474 | /// let value = event.target_unchecked_into::().value(); 475 | /// State { 476 | /// count: value.parse().unwrap() 477 | /// } 478 | /// .into() 479 | /// }); 480 | /// html! { 481 | /// 482 | /// } 483 | /// # ; 484 | /// # } 485 | /// ``` 486 | pub fn reduce_callback_with(&self, f: F) -> Callback 487 | where 488 | F: Fn(Rc, E) -> Rc + 'static, 489 | E: 'static, 490 | { 491 | let context = self.cx.clone(); 492 | Callback::from(move |e: E| { 493 | context.reduce(|x| f(x, e)); 494 | }) 495 | } 496 | 497 | /// Mutate state with given function. 498 | /// 499 | /// ``` 500 | /// # use yew::prelude::*; 501 | /// # use yewdux::prelude::*; 502 | /// # #[derive(Default, Clone, PartialEq, Eq, Store)] 503 | /// # struct State { 504 | /// # count: u32, 505 | /// # } 506 | /// # fn main() { 507 | /// # // Context handling code is omitted for clarity 508 | /// # let cx = yewdux::Context::new(); 509 | /// # let dispatch = Dispatch::::new(&cx); 510 | /// // Mutate the state in-place 511 | /// dispatch.reduce_mut(|state| state.count += 1); 512 | /// # } 513 | /// ``` 514 | pub fn reduce_mut(&self, f: F) -> R 515 | where 516 | S: Clone, 517 | F: FnOnce(&mut S) -> R, 518 | { 519 | let mut result = None; 520 | 521 | self.cx.reduce_mut(|x| { 522 | result = Some(f(x)); 523 | }); 524 | 525 | result.expect("result not initialized") 526 | } 527 | 528 | /// Like [Self::reduce_mut] but from a callback. 529 | /// 530 | /// ``` 531 | /// # use yew::prelude::*; 532 | /// # use yewdux::prelude::*; 533 | /// # #[derive(Default, Clone, PartialEq, Eq, Store)] 534 | /// # struct State { 535 | /// # count: u32, 536 | /// # } 537 | /// # fn main() { 538 | /// # // Context handling code is omitted for clarity 539 | /// # let cx = yewdux::Context::new(); 540 | /// # let dispatch = Dispatch::::new(&cx); 541 | /// // Create a callback that will mutate the state in-place when triggered 542 | /// let onclick = dispatch.reduce_mut_callback(|s| s.count += 1); 543 | /// html! { 544 | /// 545 | /// } 546 | /// # ; 547 | /// # } 548 | /// ``` 549 | pub fn reduce_mut_callback(&self, f: F) -> Callback 550 | where 551 | S: Clone, 552 | F: Fn(&mut S) -> R + 'static, 553 | E: 'static, 554 | { 555 | let context = self.cx.clone(); 556 | Callback::from(move |_| { 557 | context.reduce_mut(|x| { 558 | f(x); 559 | }); 560 | }) 561 | } 562 | 563 | /// Similar to [Self::reduce_mut_callback] but also provides the fired event. 564 | /// 565 | /// ``` 566 | /// # use yew::prelude::*; 567 | /// # use yewdux::prelude::*; 568 | /// # #[derive(Default, Clone, PartialEq, Eq, Store)] 569 | /// # struct State { 570 | /// # count: u32, 571 | /// # } 572 | /// # fn main() { 573 | /// # // Context handling code is omitted for clarity 574 | /// # let cx = yewdux::Context::new(); 575 | /// # let dispatch = Dispatch::::new(&cx); 576 | /// // Create a callback that will mutate the state using event data 577 | /// let onchange = dispatch.reduce_mut_callback_with(|state, event: Event| { 578 | /// let value = event.target_unchecked_into::().value(); 579 | /// state.count = value.parse().unwrap(); 580 | /// }); 581 | /// html! { 582 | /// 583 | /// } 584 | /// # ; 585 | /// # } 586 | /// ``` 587 | pub fn reduce_mut_callback_with(&self, f: F) -> Callback 588 | where 589 | S: Clone, 590 | F: Fn(&mut S, E) -> R + 'static, 591 | E: 'static, 592 | { 593 | let context = self.cx.clone(); 594 | Callback::from(move |e: E| { 595 | context.reduce_mut(|x| { 596 | f(x, e); 597 | }); 598 | }) 599 | } 600 | } 601 | 602 | impl Clone for Dispatch { 603 | fn clone(&self) -> Self { 604 | Self { 605 | _subscriber_id: self._subscriber_id.clone(), 606 | cx: self.cx.clone(), 607 | } 608 | } 609 | } 610 | 611 | impl PartialEq for Dispatch { 612 | fn eq(&self, other: &Self) -> bool { 613 | match (&self._subscriber_id, &other._subscriber_id) { 614 | (Some(a), Some(b)) => Rc::ptr_eq(a, b), 615 | _ => false, 616 | } 617 | } 618 | } 619 | 620 | #[cfg(test)] 621 | mod tests { 622 | 623 | use crate::{mrc::Mrc, subscriber::Subscribers}; 624 | 625 | use super::*; 626 | 627 | #[derive(Clone, PartialEq, Eq)] 628 | struct TestState(u32); 629 | impl Store for TestState { 630 | fn new(_cx: &Context) -> Self { 631 | Self(0) 632 | } 633 | 634 | fn should_notify(&self, other: &Self) -> bool { 635 | self != other 636 | } 637 | } 638 | #[derive(PartialEq, Eq)] 639 | struct TestStateNoClone(u32); 640 | impl Store for TestStateNoClone { 641 | fn new(_cx: &Context) -> Self { 642 | Self(0) 643 | } 644 | 645 | fn should_notify(&self, other: &Self) -> bool { 646 | self != other 647 | } 648 | } 649 | 650 | struct Msg; 651 | impl Reducer for Msg { 652 | fn apply(self, state: Rc) -> Rc { 653 | TestState(state.0 + 1).into() 654 | } 655 | } 656 | 657 | #[test] 658 | fn apply_no_clone() { 659 | Dispatch::new(&Context::new()).reduce(|_| TestStateNoClone(1).into()); 660 | } 661 | 662 | #[test] 663 | fn reduce_changes_value() { 664 | let dispatch = Dispatch::::new(&Context::new()); 665 | 666 | let old = dispatch.get(); 667 | 668 | dispatch.reduce(|_| TestState(1).into()); 669 | 670 | let new = dispatch.get(); 671 | 672 | assert!(old != new); 673 | } 674 | 675 | #[test] 676 | fn reduce_mut_changes_value() { 677 | let dispatch = Dispatch::::new(&Context::new()); 678 | let old = dispatch.get(); 679 | 680 | dispatch.reduce_mut(|state| *state = TestState(1)); 681 | 682 | let new = dispatch.get(); 683 | 684 | assert!(old != new); 685 | } 686 | 687 | #[test] 688 | fn reduce_does_not_require_static() { 689 | let val = "1".to_string(); 690 | Dispatch::new(&Context::new()).reduce(|_| TestState(val.parse().unwrap()).into()); 691 | } 692 | 693 | #[test] 694 | fn reduce_mut_does_not_require_static() { 695 | let val = "1".to_string(); 696 | Dispatch::new(&Context::new()) 697 | .reduce_mut(|state: &mut TestState| state.0 = val.parse().unwrap()); 698 | } 699 | 700 | #[test] 701 | fn set_changes_value() { 702 | let dispatch = Dispatch::::new(&Context::new()); 703 | 704 | let old = dispatch.get(); 705 | 706 | dispatch.set(TestState(1)); 707 | 708 | let new = dispatch.get(); 709 | 710 | assert!(old != new); 711 | } 712 | 713 | #[test] 714 | fn apply_changes_value() { 715 | let dispatch = Dispatch::::new(&Context::new()); 716 | let old = dispatch.get(); 717 | 718 | dispatch.apply(Msg); 719 | 720 | let new = dispatch.get(); 721 | 722 | assert!(old != new); 723 | } 724 | 725 | #[test] 726 | fn dispatch_set_works() { 727 | let dispatch = Dispatch::::new(&Context::new()); 728 | let old = dispatch.get(); 729 | 730 | dispatch.set(TestState(1)); 731 | 732 | assert!(dispatch.get() != old) 733 | } 734 | 735 | #[test] 736 | fn dispatch_set_callback_works() { 737 | let dispatch = Dispatch::::new(&Context::new()); 738 | let old = dispatch.get(); 739 | 740 | let cb = dispatch.set_callback(|_| TestState(1)); 741 | cb.emit(()); 742 | 743 | assert!(dispatch.get() != old) 744 | } 745 | 746 | #[test] 747 | fn dispatch_reduce_mut_works() { 748 | let dispatch = Dispatch::::new(&Context::new()); 749 | let old = dispatch.get(); 750 | 751 | dispatch.reduce_mut(|state| state.0 += 1); 752 | 753 | assert!(dispatch.get() != old) 754 | } 755 | 756 | #[test] 757 | fn dispatch_reduce_works() { 758 | let dispatch = Dispatch::::new(&Context::new()); 759 | let old = dispatch.get(); 760 | 761 | dispatch.reduce(|_| TestState(1).into()); 762 | 763 | assert!(dispatch.get() != old) 764 | } 765 | 766 | #[test] 767 | fn dispatch_reduce_callback_works() { 768 | let dispatch = Dispatch::::new(&Context::new()); 769 | let old = dispatch.get(); 770 | 771 | let cb = dispatch.reduce_callback(|_| TestState(1).into()); 772 | cb.emit(()); 773 | 774 | assert!(dispatch.get() != old) 775 | } 776 | 777 | #[test] 778 | fn dispatch_reduce_mut_callback_works() { 779 | let dispatch = Dispatch::::new(&Context::new()); 780 | let old = dispatch.get(); 781 | 782 | let cb = dispatch.reduce_mut_callback(|state| state.0 += 1); 783 | cb.emit(()); 784 | 785 | assert!(dispatch.get() != old) 786 | } 787 | 788 | #[test] 789 | fn dispatch_reduce_callback_with_works() { 790 | let dispatch = Dispatch::::new(&Context::new()); 791 | let old = dispatch.get(); 792 | 793 | let cb = dispatch.reduce_callback_with(|_, _| TestState(1).into()); 794 | cb.emit(1); 795 | 796 | assert!(dispatch.get() != old) 797 | } 798 | 799 | #[test] 800 | fn dispatch_reduce_mut_callback_with_works() { 801 | let dispatch = Dispatch::::new(&Context::new()); 802 | let old = dispatch.get(); 803 | 804 | let cb = dispatch.reduce_mut_callback_with(|state, val| state.0 += val); 805 | cb.emit(1); 806 | 807 | assert!(dispatch.get() != old) 808 | } 809 | 810 | #[test] 811 | fn dispatch_apply_works() { 812 | let dispatch = Dispatch::::new(&Context::new()); 813 | let old = dispatch.get(); 814 | 815 | dispatch.apply(Msg); 816 | 817 | assert!(dispatch.get() != old) 818 | } 819 | 820 | #[test] 821 | fn dispatch_apply_callback_works() { 822 | let dispatch = Dispatch::::new(&Context::new()); 823 | let old = dispatch.get(); 824 | 825 | let cb = dispatch.apply_callback(|_| Msg); 826 | cb.emit(()); 827 | 828 | assert!(dispatch.get() != old) 829 | } 830 | 831 | #[test] 832 | fn subscriber_is_notified() { 833 | let cx = Context::new(); 834 | let flag = Mrc::new(false); 835 | 836 | let _id = { 837 | let flag = flag.clone(); 838 | Dispatch::::new(&cx) 839 | .subscribe(move |_| flag.clone().with_mut(|flag| *flag = true)) 840 | }; 841 | 842 | *flag.borrow_mut() = false; 843 | 844 | Dispatch::::new(&cx).reduce_mut(|state| state.0 += 1); 845 | 846 | assert!(*flag.borrow()); 847 | } 848 | 849 | #[test] 850 | fn subscriber_is_not_notified_when_state_is_same() { 851 | let cx = Context::new(); 852 | let flag = Mrc::new(false); 853 | let dispatch = Dispatch::::new(&cx); 854 | 855 | // TestState(1) 856 | dispatch.reduce_mut(|_| {}); 857 | 858 | let _id = { 859 | let flag = flag.clone(); 860 | Dispatch::::new(&cx) 861 | .subscribe(move |_| flag.clone().with_mut(|flag| *flag = true)) 862 | }; 863 | 864 | *flag.borrow_mut() = false; 865 | 866 | // TestState(1) 867 | dispatch.reduce_mut(|state| state.0 = 0); 868 | 869 | assert!(!*flag.borrow()); 870 | } 871 | 872 | #[test] 873 | fn dispatch_unsubscribes_when_dropped() { 874 | let cx = Context::new(); 875 | let entry = cx.get_or_init_default::>>(); 876 | 877 | assert!(entry.store.borrow().borrow().0.is_empty()); 878 | 879 | let dispatch = Dispatch::::new(&cx).subscribe(|_| ()); 880 | 881 | assert!(!entry.store.borrow().borrow().0.is_empty()); 882 | 883 | drop(dispatch); 884 | 885 | assert!(entry.store.borrow().borrow().0.is_empty()); 886 | } 887 | 888 | #[test] 889 | fn dispatch_clone_and_original_unsubscribe_when_both_dropped() { 890 | let cx = Context::new(); 891 | let entry = cx.get_or_init_default::>>(); 892 | 893 | assert!(entry.store.borrow().borrow().0.is_empty()); 894 | 895 | let dispatch = Dispatch::::new(&cx).subscribe(|_| ()); 896 | let dispatch_clone = dispatch.clone(); 897 | 898 | assert!(!entry.store.borrow().borrow().0.is_empty()); 899 | 900 | drop(dispatch_clone); 901 | 902 | assert!(!entry.store.borrow().borrow().0.is_empty()); 903 | 904 | drop(dispatch); 905 | 906 | assert!(entry.store.borrow().borrow().0.is_empty()); 907 | } 908 | } 909 | -------------------------------------------------------------------------------- /crates/yewdux/src/functional.rs: -------------------------------------------------------------------------------- 1 | //! The functional interface for Yewdux 2 | use std::{ops::Deref, rc::Rc}; 3 | 4 | use yew::functional::*; 5 | 6 | use crate::{dispatch::Dispatch, store::Store, Context}; 7 | 8 | #[hook] 9 | fn use_cx() -> Context { 10 | #[cfg(target_arch = "wasm32")] 11 | { 12 | use_context::().unwrap_or_else(crate::context::Context::global) 13 | } 14 | #[cfg(not(target_arch = "wasm32"))] 15 | { 16 | use_context::().expect("YewduxRoot not found") 17 | } 18 | } 19 | 20 | #[hook] 21 | pub fn use_dispatch() -> Dispatch 22 | where 23 | S: Store, 24 | { 25 | Dispatch::new(&use_cx()) 26 | } 27 | 28 | /// This hook allows accessing the state of a store. When the store is modified, a re-render is 29 | /// automatically triggered. 30 | /// 31 | /// # Example 32 | /// ``` 33 | /// use yew::prelude::*; 34 | /// use yewdux::prelude::*; 35 | /// 36 | /// #[derive(Default, Clone, PartialEq, Store)] 37 | /// struct State { 38 | /// count: u32, 39 | /// } 40 | /// 41 | /// #[function_component] 42 | /// fn App() -> Html { 43 | /// let (state, dispatch) = use_store::(); 44 | /// let onclick = dispatch.reduce_mut_callback(|s| s.count += 1); 45 | /// html! { 46 | /// <> 47 | ///

{ state.count }

48 | /// 49 | /// 50 | /// } 51 | /// } 52 | /// ``` 53 | #[hook] 54 | pub fn use_store() -> (Rc, Dispatch) 55 | where 56 | S: Store, 57 | { 58 | let dispatch = use_dispatch::(); 59 | let state: UseStateHandle> = use_state(|| dispatch.get()); 60 | let dispatch = { 61 | let state = state.clone(); 62 | use_state(move || dispatch.subscribe_silent(move |val| state.set(val))) 63 | }; 64 | 65 | (Rc::clone(&state), dispatch.deref().clone()) 66 | } 67 | 68 | /// Simliar to ['use_store'], but only provides the state. 69 | #[hook] 70 | pub fn use_store_value() -> Rc 71 | where 72 | S: Store, 73 | { 74 | let (state, _dispatch) = use_store(); 75 | 76 | state 77 | } 78 | 79 | /// Provides access to some derived portion of state. Useful when you only want to rerender 80 | /// when that portion has changed. 81 | /// 82 | /// # Example 83 | /// ``` 84 | /// use yew::prelude::*; 85 | /// use yewdux::prelude::*; 86 | /// 87 | /// #[derive(Default, Clone, PartialEq, Store)] 88 | /// struct State { 89 | /// count: u32, 90 | /// } 91 | /// 92 | /// #[function_component] 93 | /// fn App() -> Html { 94 | /// let dispatch = use_dispatch::(); 95 | /// let count = use_selector(|state: &State| state.count); 96 | /// let onclick = dispatch.reduce_mut_callback(|state| state.count += 1); 97 | /// 98 | /// html! { 99 | /// <> 100 | ///

{ *count }

101 | /// 102 | /// 103 | /// } 104 | /// } 105 | /// ``` 106 | #[hook] 107 | pub fn use_selector(selector: F) -> Rc 108 | where 109 | S: Store, 110 | R: PartialEq + 'static, 111 | F: Fn(&S) -> R + 'static, 112 | { 113 | use_selector_eq(selector, |a, b| a == b) 114 | } 115 | 116 | /// Similar to [`use_selector`], with the additional flexibility of a custom equality check for 117 | /// selected value. 118 | #[hook] 119 | pub fn use_selector_eq(selector: F, eq: E) -> Rc 120 | where 121 | S: Store, 122 | R: 'static, 123 | F: Fn(&S) -> R + 'static, 124 | E: Fn(&R, &R) -> bool + 'static, 125 | { 126 | use_selector_eq_with_deps(move |state, _| selector(state), eq, ()) 127 | } 128 | 129 | /// Similar to [`use_selector`], but also allows for dependencies from environment. This is 130 | /// necessary when the derived value uses some captured value. 131 | /// 132 | /// # Example 133 | /// ``` 134 | /// use std::collections::HashMap; 135 | /// 136 | /// use yew::prelude::*; 137 | /// use yewdux::prelude::*; 138 | /// 139 | /// #[derive(Default, Clone, PartialEq, Store)] 140 | /// struct State { 141 | /// user_names: HashMap, 142 | /// } 143 | /// 144 | /// #[derive(Properties, PartialEq, Clone)] 145 | /// struct AppProps { 146 | /// user_id: u32, 147 | /// } 148 | /// 149 | /// #[function_component] 150 | /// fn ViewName(&AppProps { user_id }: &AppProps) -> Html { 151 | /// let user_name = use_selector_with_deps( 152 | /// |state: &State, id| state.user_names.get(id).cloned().unwrap_or_default(), 153 | /// user_id, 154 | /// ); 155 | /// 156 | /// html! { 157 | ///

158 | /// { user_name } 159 | ///

160 | /// } 161 | /// } 162 | /// ``` 163 | #[hook] 164 | pub fn use_selector_with_deps(selector: F, deps: D) -> Rc 165 | where 166 | S: Store, 167 | R: PartialEq + 'static, 168 | D: Clone + PartialEq + 'static, 169 | F: Fn(&S, &D) -> R + 'static, 170 | { 171 | use_selector_eq_with_deps(selector, |a, b| a == b, deps) 172 | } 173 | 174 | /// Similar to [`use_selector_with_deps`], but also allows an equality function, similar to 175 | /// [`use_selector_eq`] 176 | #[hook] 177 | pub fn use_selector_eq_with_deps(selector: F, eq: E, deps: D) -> Rc 178 | where 179 | S: Store, 180 | R: 'static, 181 | D: Clone + PartialEq + 'static, 182 | F: Fn(&S, &D) -> R + 'static, 183 | E: Fn(&R, &R) -> bool + 'static, 184 | { 185 | let dispatch = use_dispatch::(); 186 | // Given to user, this is what we update to force a re-render. 187 | let selected = { 188 | let state = dispatch.get(); 189 | let value = selector(&state, &deps); 190 | 191 | use_state(|| Rc::new(value)) 192 | }; 193 | // Local tracking value, because `selected` isn't updated in our subscriber scope. 194 | let current = { 195 | let value = Rc::clone(&selected); 196 | use_mut_ref(|| value) 197 | }; 198 | 199 | let _dispatch = { 200 | let selected = selected.clone(); 201 | use_memo(deps, move |deps| { 202 | let deps = deps.clone(); 203 | dispatch.subscribe(move |val: Rc| { 204 | let value = selector(&val, &deps); 205 | 206 | if !eq(¤t.borrow(), &value) { 207 | let value = Rc::new(value); 208 | // Update value for user. 209 | selected.set(Rc::clone(&value)); 210 | // Make sure to update our tracking value too. 211 | *current.borrow_mut() = Rc::clone(&value); 212 | } 213 | }) 214 | }) 215 | }; 216 | 217 | Rc::clone(&selected) 218 | } 219 | -------------------------------------------------------------------------------- /crates/yewdux/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Yewdux 2 | //! 3 | //! Simple state management for [Yew](https://yew.rs) applications. 4 | //! 5 | //! See the [book](https://intendednull.github.io/yewdux/) for more details. 6 | //! 7 | //! ## Example 8 | //! 9 | //! ```rust 10 | //! use yew::prelude::*; 11 | //! use yewdux::prelude::*; 12 | //! 13 | //! #[derive(Default, Clone, PartialEq, Eq, Store)] 14 | //! struct State { 15 | //! count: u32, 16 | //! } 17 | //! 18 | //! #[function_component] 19 | //! fn App() -> Html { 20 | //! let (state, dispatch) = use_store::(); 21 | //! let onclick = dispatch.reduce_mut_callback(|state| state.count += 1); 22 | //! 23 | //! html! { 24 | //! <> 25 | //!

{ state.count }

26 | //! 27 | //! 28 | //! } 29 | //! } 30 | //! ``` 31 | #![allow(clippy::needless_doctest_main)] 32 | 33 | mod anymap; 34 | pub mod context; 35 | pub mod context_provider; 36 | pub mod derived_from; 37 | pub mod dispatch; 38 | pub mod functional; 39 | pub mod listener; 40 | pub mod mrc; 41 | #[cfg(any(feature = "doctests", target_arch = "wasm32"))] 42 | pub mod storage; 43 | pub mod store; 44 | mod subscriber; 45 | 46 | // Used by macro. 47 | #[doc(hidden)] 48 | pub use log; 49 | 50 | // Allow shorthand, like `yewdux::Dispatch` 51 | pub use context::Context; 52 | pub use prelude::*; 53 | 54 | pub mod prelude { 55 | //! Default exports 56 | 57 | pub use crate::{ 58 | context_provider::YewduxRoot, 59 | derived_from::{DerivedFrom, DerivedFromMut}, 60 | dispatch::Dispatch, 61 | functional::{ 62 | use_dispatch, use_selector, use_selector_eq, use_selector_eq_with_deps, 63 | use_selector_with_deps, use_store, use_store_value, 64 | }, 65 | listener::{init_listener, Listener}, 66 | store::{Reducer, Store}, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /crates/yewdux/src/listener.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::{context::Context, dispatch::Dispatch, store::Store}; 4 | 5 | /// Listens to [Store](crate::store::Store) changes. 6 | pub trait Listener: 'static { 7 | type Store: Store; 8 | 9 | fn on_change(&self, cx: &Context, state: Rc); 10 | } 11 | 12 | #[allow(unused)] 13 | struct ListenerStore(Dispatch); 14 | impl Store for ListenerStore { 15 | fn new(_cx: &Context) -> Self { 16 | // This is a private type, and only ever constructed by `init_listener` with a manual 17 | // constructor, so this should never run. 18 | unreachable!() 19 | } 20 | 21 | fn should_notify(&self, _other: &Self) -> bool { 22 | false 23 | } 24 | } 25 | 26 | /// Initiate a [Listener]. Does nothing if listener is already initiated. 27 | pub fn init_listener L>(new_listener: F, cx: &Context) { 28 | cx.get_or_init(|cx| { 29 | let dispatch = { 30 | let listener = new_listener(); 31 | let cx = cx.clone(); 32 | Dispatch::new(&cx).subscribe_silent(move |state| listener.on_change(&cx, state)) 33 | }; 34 | 35 | ListenerStore::(dispatch) 36 | }); 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | 42 | use std::cell::Cell; 43 | 44 | use super::*; 45 | 46 | #[derive(Clone, PartialEq, Eq)] 47 | struct TestState(u32); 48 | impl Store for TestState { 49 | fn new(_cx: &Context) -> Self { 50 | Self(0) 51 | } 52 | 53 | fn should_notify(&self, other: &Self) -> bool { 54 | self != other 55 | } 56 | } 57 | 58 | #[derive(Clone)] 59 | struct TestListener(Rc>); 60 | impl Listener for TestListener { 61 | type Store = TestState; 62 | 63 | fn on_change(&self, _cx: &Context, state: Rc) { 64 | self.0.set(state.0); 65 | } 66 | } 67 | 68 | #[derive(Clone)] 69 | struct AnotherTestListener(Rc>); 70 | impl Listener for AnotherTestListener { 71 | type Store = TestState; 72 | 73 | fn on_change(&self, _cx: &Context, state: Rc) { 74 | self.0.set(state.0); 75 | } 76 | } 77 | 78 | #[derive(Clone, PartialEq, Eq)] 79 | struct TestState2; 80 | impl Store for TestState2 { 81 | fn new(cx: &Context) -> Self { 82 | init_listener(|| TestListener2, cx); 83 | Self 84 | } 85 | 86 | fn should_notify(&self, other: &Self) -> bool { 87 | self != other 88 | } 89 | } 90 | 91 | #[derive(Clone)] 92 | struct TestListener2; 93 | impl Listener for TestListener2 { 94 | type Store = TestState2; 95 | 96 | fn on_change(&self, _cx: &Context, _state: Rc) {} 97 | } 98 | 99 | #[derive(Clone, PartialEq, Eq)] 100 | struct TestStateRecursive(u32); 101 | impl Store for TestStateRecursive { 102 | fn new(_cx: &Context) -> Self { 103 | Self(0) 104 | } 105 | 106 | fn should_notify(&self, other: &Self) -> bool { 107 | self != other 108 | } 109 | } 110 | 111 | #[derive(Clone)] 112 | struct TestListenerRecursive; 113 | impl Listener for TestListenerRecursive { 114 | type Store = TestStateRecursive; 115 | 116 | fn on_change(&self, cx: &Context, state: Rc) { 117 | let dispatch = Dispatch::::new(cx); 118 | if state.0 < 10 { 119 | dispatch.reduce_mut(|state| state.0 += 1); 120 | } 121 | } 122 | } 123 | 124 | #[test] 125 | fn recursion() { 126 | let cx = Context::new(); 127 | init_listener(|| TestListenerRecursive, &cx); 128 | let dispatch = Dispatch::::new(&cx); 129 | dispatch.reduce_mut(|state| state.0 = 1); 130 | assert_eq!(dispatch.get().0, 10); 131 | } 132 | 133 | #[test] 134 | fn listener_is_called() { 135 | let cx = Context::new(); 136 | let listener = TestListener(Default::default()); 137 | 138 | init_listener(|| listener.clone(), &cx); 139 | 140 | Dispatch::new(&cx).reduce_mut(|state: &mut TestState| state.0 = 1); 141 | 142 | assert_eq!(listener.0.get(), 1) 143 | } 144 | 145 | #[test] 146 | fn listener_is_not_replaced() { 147 | let cx = Context::new(); 148 | let listener1 = TestListener(Default::default()); 149 | let listener2 = TestListener(Default::default()); 150 | 151 | init_listener(|| listener1.clone(), &cx); 152 | 153 | Dispatch::new(&cx).reduce_mut(|state: &mut TestState| state.0 = 1); 154 | 155 | assert_eq!(listener1.0.get(), 1); 156 | 157 | init_listener(|| listener2.clone(), &cx); 158 | 159 | Dispatch::new(&cx).reduce_mut(|state: &mut TestState| state.0 = 2); 160 | 161 | assert_eq!(listener1.0.get(), 2); 162 | assert_eq!(listener2.0.get(), 0); 163 | } 164 | 165 | #[test] 166 | fn listener_of_different_type_is_not_replaced() { 167 | let cx = Context::new(); 168 | let listener1 = TestListener(Default::default()); 169 | let listener2 = AnotherTestListener(Default::default()); 170 | 171 | init_listener(|| listener1.clone(), &cx); 172 | 173 | cx.reduce_mut(|state: &mut TestState| state.0 = 1); 174 | 175 | assert_eq!(listener1.0.get(), 1); 176 | 177 | init_listener(|| listener2.clone(), &cx); 178 | 179 | cx.reduce_mut(|state: &mut TestState| state.0 = 2); 180 | 181 | assert_eq!(listener1.0.get(), 2); 182 | assert_eq!(listener2.0.get(), 2); 183 | } 184 | 185 | #[test] 186 | fn can_init_listener_from_store() { 187 | let cx = Context::new(); 188 | cx.get::(); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /crates/yewdux/src/mrc.rs: -------------------------------------------------------------------------------- 1 | //! Mutable reference counted wrapper type that works well with Yewdux. 2 | //! 3 | //! Useful when you don't want to implement `Clone` or `PartialEq` for a type. 4 | //! 5 | //! ``` 6 | //! # use yewdux::mrc::Mrc; 7 | //! # fn main() { 8 | //! let expensive_data = Mrc::new("Some long string that shouldn't be cloned.".to_string()); 9 | //! let old_ref = expensive_data.clone(); 10 | //! 11 | //! // They are equal (for now). 12 | //! assert!(expensive_data == old_ref); 13 | //! 14 | //! // Here we use interior mutability to change the inner value. Doing so will mark the 15 | //! // container as changed. 16 | //! *expensive_data.borrow_mut() += " Here we can modify our expensive data."; 17 | //! 18 | //! // Once marked as changed, it will cause any equality check to fail (forcing a re-render). 19 | //! assert!(expensive_data != old_ref); 20 | //! // The underlying state is the same though. 21 | //! assert!(*expensive_data.borrow() == *old_ref.borrow()); 22 | //! # } 23 | //! ``` 24 | 25 | use std::{ 26 | cell::{Cell, RefCell}, 27 | ops::{Deref, DerefMut}, 28 | rc::Rc, 29 | }; 30 | 31 | use serde::{Deserialize, Serialize}; 32 | 33 | use crate::{store::Store, Context}; 34 | 35 | fn nonce() -> u32 { 36 | thread_local! { 37 | static NONCE: Cell = Default::default(); 38 | } 39 | 40 | NONCE 41 | .try_with(|nonce| { 42 | nonce.set(nonce.get().wrapping_add(1)); 43 | nonce.get() 44 | }) 45 | .expect("NONCE thread local key init failed") 46 | } 47 | 48 | /// Mutable reference counted wrapper type that works well with Yewdux. 49 | /// 50 | /// This is basically a wrapper over `Rc>`, with the notable difference of simple change 51 | /// detection (so it works with Yewdux). Whenever this type borrows mutably, it is marked as 52 | /// changed. Because there is no way to detect whether it has actually changed or not, it is up to 53 | /// the user to prevent unecessary re-renders. 54 | #[derive(Debug, Serialize, Deserialize)] 55 | pub struct Mrc { 56 | inner: Rc>, 57 | nonce: Cell, 58 | } 59 | 60 | impl Mrc { 61 | pub fn new(value: T) -> Self { 62 | Self { 63 | inner: Rc::new(RefCell::new(value)), 64 | nonce: Cell::new(nonce()), 65 | } 66 | } 67 | 68 | pub fn with_mut(&self, f: impl FnOnce(&mut T) -> R) -> R { 69 | let mut this = self.borrow_mut(); 70 | f(this.deref_mut()) 71 | } 72 | 73 | pub fn borrow(&self) -> impl Deref + '_ { 74 | self.inner.borrow() 75 | } 76 | 77 | /// Provide a mutable reference to inner value. 78 | pub fn borrow_mut(&self) -> impl DerefMut + '_ { 79 | // Mark as changed. 80 | self.nonce.set(nonce()); 81 | self.inner.borrow_mut() 82 | } 83 | } 84 | 85 | impl Store for Mrc { 86 | fn new(cx: &Context) -> Self { 87 | T::new(cx).into() 88 | } 89 | 90 | fn should_notify(&self, other: &Self) -> bool { 91 | self != other 92 | } 93 | } 94 | 95 | impl Default for Mrc { 96 | fn default() -> Self { 97 | Self::new(Default::default()) 98 | } 99 | } 100 | 101 | impl Clone for Mrc { 102 | fn clone(&self) -> Self { 103 | Self { 104 | inner: Rc::clone(&self.inner), 105 | nonce: self.nonce.clone(), 106 | } 107 | } 108 | } 109 | 110 | impl From for Mrc { 111 | fn from(value: T) -> Self { 112 | Self::new(value) 113 | } 114 | } 115 | 116 | impl PartialEq for Mrc { 117 | fn eq(&self, other: &Self) -> bool { 118 | Rc::ptr_eq(&self.inner, &other.inner) && self.nonce == other.nonce 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | 125 | use crate::{dispatch::Dispatch, store::Store, Context}; 126 | 127 | use super::*; 128 | 129 | #[derive(Clone, PartialEq)] 130 | struct TestState(Mrc); 131 | impl Store for TestState { 132 | fn new(_cx: &Context) -> Self { 133 | Self(Mrc::new(0)) 134 | } 135 | 136 | fn should_notify(&self, other: &Self) -> bool { 137 | self != other 138 | } 139 | } 140 | 141 | struct CanImplStoreForMrcDirectly; 142 | impl Store for Mrc { 143 | fn new(_cx: &Context) -> Self { 144 | CanImplStoreForMrcDirectly.into() 145 | } 146 | 147 | fn should_notify(&self, other: &Self) -> bool { 148 | self != other 149 | } 150 | } 151 | 152 | #[test] 153 | fn subscriber_is_notified_on_borrow_mut() { 154 | let flag = Mrc::new(false); 155 | let cx = Context::new(); 156 | 157 | let dispatch = { 158 | let flag = flag.clone(); 159 | Dispatch::::new(&cx) 160 | .subscribe(move |_| flag.clone().with_mut(|flag| *flag = true)) 161 | }; 162 | 163 | *flag.borrow_mut() = false; 164 | 165 | dispatch.reduce_mut(|state| { 166 | state.0.borrow_mut(); 167 | }); 168 | 169 | assert!(*flag.borrow()); 170 | } 171 | 172 | #[test] 173 | fn subscriber_is_notified_on_with_mut() { 174 | let flag = Mrc::new(false); 175 | let cx = Context::new(); 176 | 177 | let dispatch = { 178 | let flag = flag.clone(); 179 | Dispatch::::new(&cx) 180 | .subscribe(move |_| flag.clone().with_mut(|flag| *flag = true)) 181 | }; 182 | 183 | *flag.borrow_mut() = false; 184 | 185 | dispatch.reduce_mut(|state| state.0.with_mut(|_| ())); 186 | 187 | assert!(*flag.borrow()); 188 | } 189 | 190 | #[test] 191 | fn can_wrap_store_with_mrc() { 192 | let cx = Context::new(); 193 | let dispatch = Dispatch::>::new(&cx); 194 | assert!(*dispatch.get().borrow().0.borrow() == 0) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /crates/yewdux/src/storage.rs: -------------------------------------------------------------------------------- 1 | //! Store persistence through session or local storage 2 | //! 3 | //! ``` 4 | //! use std::rc::Rc; 5 | //! 6 | //! use yewdux::{prelude::*, storage}; 7 | //! 8 | //! use serde::{Deserialize, Serialize}; 9 | //! 10 | //! struct StorageListener; 11 | //! impl Listener for StorageListener { 12 | //! type Store = State; 13 | //! 14 | //! fn on_change(&mut self, _cx: &Context, state: Rc) { 15 | //! if let Err(err) = storage::save(state.as_ref(), storage::Area::Local) { 16 | //! println!("Error saving state to storage: {:?}", err); 17 | //! } 18 | //! } 19 | //! } 20 | //! 21 | //! #[derive(Default, Clone, PartialEq, Eq, Deserialize, Serialize)] 22 | //! struct State { 23 | //! count: u32, 24 | //! } 25 | //! 26 | //! impl Store for State { 27 | //! fn new(cx: &yewdux::Context) -> Self { 28 | //! init_listener(StorageListener, cx); 29 | //! 30 | //! storage::load(storage::Area::Local) 31 | //! .ok() 32 | //! .flatten() 33 | //! .unwrap_or_default() 34 | //! } 35 | //! 36 | //! fn should_notify(&self, other: &Self) -> bool { 37 | //! self != other 38 | //! } 39 | //! } 40 | //! ``` 41 | 42 | use std::{any::type_name, rc::Rc}; 43 | 44 | use serde::{de::DeserializeOwned, Serialize}; 45 | use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; 46 | use web_sys::{Event, Storage}; 47 | 48 | use crate::{dispatch::Dispatch, listener::Listener, store::Store, Context}; 49 | 50 | #[derive(Debug, thiserror::Error)] 51 | pub enum StorageError { 52 | #[error("Window not found")] 53 | WindowNotFound, 54 | #[error("Could not access {0:?} storage")] 55 | StorageAccess(Area), 56 | #[error("A web-sys error occurred")] 57 | WebSys(JsValue), 58 | #[error("A serde error occurred")] 59 | Serde(#[from] serde_json::Error), 60 | } 61 | 62 | #[derive(Debug, Clone, Copy)] 63 | pub enum Area { 64 | Local, 65 | Session, 66 | } 67 | 68 | /// A [Listener] that will save state to browser storage whenever state has changed. 69 | pub struct StorageListener { 70 | area: Area, 71 | _marker: std::marker::PhantomData, 72 | } 73 | 74 | impl StorageListener { 75 | pub fn new(area: Area) -> Self { 76 | Self { 77 | area, 78 | _marker: Default::default(), 79 | } 80 | } 81 | } 82 | 83 | impl Listener for StorageListener 84 | where 85 | T: Store + Serialize, 86 | { 87 | type Store = T; 88 | 89 | fn on_change(&self, _cx: &Context, state: Rc) { 90 | if let Err(err) = save(state.as_ref(), self.area) { 91 | crate::log::error!("Error saving state to storage: {:?}", err); 92 | } 93 | } 94 | } 95 | 96 | fn get_storage(area: Area) -> Result { 97 | let window = web_sys::window().ok_or(StorageError::WindowNotFound)?; 98 | let storage = match area { 99 | Area::Local => window.local_storage(), 100 | Area::Session => window.session_storage(), 101 | }; 102 | 103 | storage 104 | .map_err(StorageError::WebSys)? 105 | .ok_or(StorageError::StorageAccess(area)) 106 | } 107 | 108 | /// Save state to session or local storage. 109 | pub fn save(state: &T, area: Area) -> Result<(), StorageError> { 110 | let storage = get_storage(area)?; 111 | 112 | let value = &serde_json::to_string(state).map_err(StorageError::Serde)?; 113 | storage 114 | .set(type_name::(), value) 115 | .map_err(StorageError::WebSys)?; 116 | 117 | Ok(()) 118 | } 119 | 120 | /// Load state from session or local storage. 121 | pub fn load(area: Area) -> Result, StorageError> { 122 | let storage = get_storage(area)?; 123 | 124 | let value = storage 125 | .get(type_name::()) 126 | .map_err(StorageError::WebSys)?; 127 | 128 | match value { 129 | Some(value) => { 130 | let state = serde_json::from_str(&value).map_err(StorageError::Serde)?; 131 | 132 | Ok(Some(state)) 133 | } 134 | None => Ok(None), 135 | } 136 | } 137 | 138 | /// Synchronize state across all tabs. **WARNING**: This provides no protection for multiple 139 | /// calls. Doing so will result in repeated loading. Using the macro is advised. 140 | pub fn init_tab_sync( 141 | area: Area, 142 | cx: &Context, 143 | ) -> Result<(), StorageError> { 144 | let cx = cx.clone(); 145 | let closure = Closure::wrap(Box::new(move |_: &Event| match load(area) { 146 | Ok(Some(state)) => { 147 | Dispatch::::new(&cx).set(state); 148 | } 149 | Err(e) => { 150 | crate::log::error!("Unable to load state: {:?}", e); 151 | } 152 | _ => {} 153 | }) as Box); 154 | 155 | web_sys::window() 156 | .ok_or(StorageError::WindowNotFound)? 157 | .add_event_listener_with_callback("storage", closure.as_ref().unchecked_ref()) 158 | .map_err(StorageError::WebSys)?; 159 | 160 | closure.forget(); 161 | 162 | Ok(()) 163 | } 164 | 165 | #[cfg(test)] 166 | mod tests { 167 | use super::*; 168 | 169 | use serde::Deserialize; 170 | 171 | #[derive(Deserialize)] 172 | struct TestStore; 173 | impl Store for TestStore { 174 | fn new(_cx: &Context) -> Self { 175 | Self 176 | } 177 | 178 | fn should_notify(&self, _old: &Self) -> bool { 179 | true 180 | } 181 | } 182 | 183 | #[test] 184 | fn tab_sync() { 185 | init_tab_sync::(Area::Local, &Context::global()).unwrap(); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /crates/yewdux/src/store.rs: -------------------------------------------------------------------------------- 1 | //! Unique state shared application-wide 2 | use std::rc::Rc; 3 | 4 | pub use yewdux_macros::Store; 5 | 6 | use crate::Context; 7 | 8 | /// A type that holds application state. 9 | pub trait Store: 'static { 10 | /// Create this store. 11 | fn new(cx: &Context) -> Self; 12 | 13 | /// Indicate whether or not subscribers should be notified about this change. Usually this 14 | /// should be set to `self != old`. 15 | fn should_notify(&self, old: &Self) -> bool; 16 | } 17 | 18 | /// A type that can change state. 19 | /// 20 | /// ``` 21 | /// use std::rc::Rc; 22 | /// 23 | /// use yew::prelude::*; 24 | /// use yewdux::prelude::*; 25 | /// 26 | /// #[derive(Default, Clone, PartialEq, Eq, Store)] 27 | /// struct Counter { 28 | /// count: u32, 29 | /// } 30 | /// 31 | /// enum Msg { 32 | /// AddOne, 33 | /// } 34 | /// 35 | /// impl Reducer for Msg { 36 | /// fn apply(self, mut counter: Rc) -> Rc { 37 | /// let state = Rc::make_mut(&mut counter); 38 | /// match self { 39 | /// Msg::AddOne => state.count += 1, 40 | /// }; 41 | /// 42 | /// counter 43 | /// } 44 | /// } 45 | /// 46 | /// #[function_component] 47 | /// fn App() -> Html { 48 | /// let (counter, dispatch) = use_store::(); 49 | /// let onclick = dispatch.apply_callback(|_| Msg::AddOne); 50 | /// 51 | /// html! { 52 | /// <> 53 | ///

{ counter.count }

54 | /// 55 | /// 56 | /// } 57 | /// } 58 | /// ``` 59 | pub trait Reducer { 60 | /// Mutate state. 61 | fn apply(self, state: Rc) -> Rc; 62 | } 63 | 64 | impl Reducer for F 65 | where 66 | F: FnOnce(Rc) -> Rc, 67 | { 68 | fn apply(self, state: Rc) -> Rc { 69 | self(state) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/yewdux/src/subscriber.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use std::{any::Any, marker::PhantomData}; 3 | 4 | use slab::Slab; 5 | use yew::Callback; 6 | 7 | use crate::{mrc::Mrc, store::Store, Context}; 8 | 9 | pub(crate) struct Subscribers(pub(crate) Slab>>); 10 | 11 | impl Store for Subscribers { 12 | fn new(_cx: &Context) -> Self { 13 | Self(Default::default()) 14 | } 15 | 16 | fn should_notify(&self, other: &Self) -> bool { 17 | self != other 18 | } 19 | } 20 | 21 | impl Mrc> { 22 | pub(crate) fn subscribe>(&self, on_change: C) -> SubscriberId { 23 | let key = self.borrow_mut().0.insert(Box::new(on_change)); 24 | SubscriberId { 25 | subscribers_ref: self.clone(), 26 | key, 27 | _store_type: Default::default(), 28 | } 29 | } 30 | 31 | pub(crate) fn unsubscribe(&mut self, key: usize) { 32 | self.borrow_mut().0.remove(key); 33 | } 34 | 35 | pub(crate) fn notify(&self, state: Rc) { 36 | for (_, subscriber) in &self.borrow().0 { 37 | subscriber.call(Rc::clone(&state)); 38 | } 39 | } 40 | } 41 | 42 | impl PartialEq for Subscribers { 43 | fn eq(&self, _other: &Self) -> bool { 44 | true 45 | } 46 | } 47 | 48 | impl Default for Subscribers { 49 | fn default() -> Self { 50 | Self(Default::default()) 51 | } 52 | } 53 | 54 | /// Points to a subscriber in context. That subscriber is removed when this is dropped. 55 | pub struct SubscriberId { 56 | subscribers_ref: Mrc>, 57 | pub(crate) key: usize, 58 | pub(crate) _store_type: PhantomData, 59 | } 60 | 61 | impl std::fmt::Debug for SubscriberId { 62 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 63 | f.debug_struct("SubscriberId") 64 | .field("key", &self.key) 65 | .finish() 66 | } 67 | } 68 | 69 | impl SubscriberId { 70 | /// Leak this subscription, so it is never dropped. 71 | pub fn leak(self) { 72 | thread_local! { 73 | static LEAKED: Mrc>> = Default::default(); 74 | } 75 | 76 | LEAKED 77 | .try_with(|leaked| leaked.clone()) 78 | .expect("LEAKED thread local key init failed") 79 | .with_mut(|leaked| leaked.push(Box::new(self))); 80 | } 81 | } 82 | 83 | impl Drop for SubscriberId { 84 | fn drop(&mut self) { 85 | self.subscribers_ref.unsubscribe(self.key) 86 | } 87 | } 88 | 89 | pub trait Callable: 'static { 90 | fn call(&self, value: Rc); 91 | } 92 | 93 | impl) + 'static> Callable for F { 94 | fn call(&self, value: Rc) { 95 | self(value) 96 | } 97 | } 98 | 99 | impl Callable for Callback> { 100 | fn call(&self, value: Rc) { 101 | self.emit(value) 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | 108 | use super::*; 109 | 110 | use crate::context::Context; 111 | use crate::dispatch::Dispatch; 112 | use crate::mrc::Mrc; 113 | 114 | #[derive(Clone, PartialEq, Eq)] 115 | struct TestState(u32); 116 | impl Store for TestState { 117 | fn new(_cx: &Context) -> Self { 118 | Self(0) 119 | } 120 | 121 | fn should_notify(&self, other: &Self) -> bool { 122 | self != other 123 | } 124 | } 125 | 126 | #[test] 127 | fn subscribe_adds_to_list() { 128 | let cx = Context::new(); 129 | let entry = cx.get_or_init_default::>>(); 130 | 131 | assert!(entry.store.borrow().borrow().0.is_empty()); 132 | 133 | let _id = Dispatch::new(&cx).subscribe(|_: Rc| ()); 134 | 135 | assert!(!entry.store.borrow().borrow().0.is_empty()); 136 | } 137 | 138 | #[test] 139 | fn unsubscribe_removes_from_list() { 140 | let cx = Context::new(); 141 | let entry = cx.get_or_init_default::>>(); 142 | 143 | assert!(entry.store.borrow().borrow().0.is_empty()); 144 | 145 | let id = Dispatch::new(&cx).subscribe(|_: Rc| ()); 146 | 147 | assert!(!entry.store.borrow().borrow().0.is_empty()); 148 | 149 | drop(id); 150 | 151 | assert!(entry.store.borrow().borrow().0.is_empty()); 152 | } 153 | 154 | #[test] 155 | fn subscriber_id_unsubscribes_when_dropped() { 156 | let cx = Context::new(); 157 | let entry = cx.get_or_init_default::>>(); 158 | 159 | assert!(entry.store.borrow().borrow().0.is_empty()); 160 | 161 | let id = Dispatch::::new(&cx).subscribe(|_| {}); 162 | 163 | assert!(!entry.store.borrow().borrow().0.is_empty()); 164 | 165 | drop(id); 166 | 167 | assert!(entry.store.borrow().borrow().0.is_empty()); 168 | } 169 | 170 | #[test] 171 | fn subscriber_is_notified_on_subscribe() { 172 | let flag = Mrc::new(false); 173 | let cx = Context::new(); 174 | 175 | let _id = { 176 | let flag = flag.clone(); 177 | Dispatch::::new(&cx) 178 | .subscribe(move |_| flag.clone().with_mut(|flag| *flag = true)) 179 | }; 180 | 181 | assert!(*flag.borrow()); 182 | } 183 | 184 | #[test] 185 | fn subscriber_is_notified_after_leak() { 186 | let flag = Mrc::new(false); 187 | let cx = Context::new(); 188 | 189 | let id = { 190 | let flag = flag.clone(); 191 | cx.subscribe::(move |_| flag.clone().with_mut(|flag| *flag = true)) 192 | }; 193 | 194 | *flag.borrow_mut() = false; 195 | 196 | id.leak(); 197 | 198 | cx.reduce_mut(|state: &mut TestState| state.0 += 1); 199 | 200 | assert!(*flag.borrow()); 201 | } 202 | 203 | #[test] 204 | fn can_modify_state_inside_on_changed() { 205 | let cx = Context::new(); 206 | let cxo = cx.clone(); 207 | let dispatch = Dispatch::::new(&cx).subscribe(move |state: Rc| { 208 | if state.0 == 0 { 209 | Dispatch::new(&cxo).reduce_mut(|state: &mut TestState| state.0 += 1); 210 | } 211 | }); 212 | 213 | assert_eq!(dispatch.get().0, 1) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book/ 2 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Noah"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Yewdux" 7 | 8 | [rust] 9 | edition = "2021" 10 | 11 | [output.html.playground] 12 | runnable = true 13 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](./intro.md) 4 | 5 | # Quickstart 6 | 7 | - [Setup](./setup.md) 8 | - [Example](./example.md) 9 | 10 | # Usage 11 | 12 | - [Store](./store.md) 13 | - [Default value](./default_store.md) 14 | - [Persistence](./persistence.md) 15 | - [Derived State](./derived_state.md) 16 | - [Writing state](./dispatch.md) 17 | - [Reading state](./reading.md) 18 | - [Listeners](./listeners.md) 19 | - [Contexts](./context.md) 20 | - [SSR Support](./ssr.md) 21 | -------------------------------------------------------------------------------- /docs/src/context.md: -------------------------------------------------------------------------------- 1 | # Contexts 2 | 3 | Contexts contains the state of your Stores. You rarely (if ever) need to manage them manually, but 4 | it's useful to understand how they work. 5 | 6 | You can easily create a new local context with `Context::new`. Then just pass it into a dispatch and 7 | you have your very own locally managed store! 8 | 9 | ```rust 10 | # extern crate yew; 11 | # extern crate yewdux; 12 | use yew::prelude::*; 13 | use yewdux::prelude::*; 14 | 15 | #[derive(Clone, PartialEq, Default, Store)] 16 | struct Counter(u32); 17 | 18 | let cx = yewdux::Context::new(); 19 | let dispatch = Dispatch::::new(&cx); 20 | ``` 21 | 22 | Changes to one context are not reflected in any others: 23 | 24 | ```rust 25 | # extern crate yewdux; 26 | # use yewdux::prelude::*; 27 | # #[derive(Clone, PartialEq, Default, Store)] 28 | # struct Counter(u32); 29 | let cx_1 = yewdux::Context::new(); 30 | let dispatch_1 = Dispatch::::new(&cx_1); 31 | 32 | let cx_2 = yewdux::Context::new(); 33 | let dispatch_2 = Dispatch::::new(&cx_2); 34 | 35 | dispatch_1.set(Counter(1)); 36 | dispatch_2.set(Counter(2)); 37 | 38 | assert!(dispatch_1.get() != dispatch_2.get()); 39 | ``` 40 | 41 | ## The Global Context 42 | 43 | You may already be familar with the global context. This is what you are using when you create a 44 | dispatch with `Dispatch::global`. The global context is thread-local, meaning you can access it from 45 | anywhere in your code as long as it's on the same thread (for wasm this is effectively everywhere). 46 | 47 | ```rust 48 | # extern crate yewdux; 49 | # use yewdux::prelude::*; 50 | # #[derive(Clone, PartialEq, Default, Store)] 51 | # struct Counter(u32); 52 | // These are equivalent! 53 | let dispatch_1 = Dispatch::::global(); 54 | let dispatch_2 = Dispatch::::new(&yewdux::Context::global()); 55 | 56 | dispatch_1.set(Counter(1)); 57 | 58 | assert!(dispatch_1.get() == dispatch_2.get()); 59 | ``` 60 | 61 | **IMPORTANT**: Use of global context is only supported for wasm targets. See [ssr support](./ssr.md) 62 | for more details. 63 | ------- 64 | 65 | -------------------------------------------------------------------------------- /docs/src/default_store.md: -------------------------------------------------------------------------------- 1 | # Setting default store values 2 | 3 | The best way to define the default value of your store is by manually implementing `Default`. 4 | 5 | ```rust 6 | # extern crate yewdux; 7 | # use yewdux::prelude::*; 8 | #[derive(PartialEq, Store)] 9 | struct MyStore { 10 | foo: String, 11 | bar: String, 12 | } 13 | 14 | impl Default for MyStore { 15 | fn default() -> Self { 16 | Self { 17 | foo: "foo".to_string(), 18 | bar: "bar".to_string(), 19 | } 20 | } 21 | } 22 | ``` 23 | 24 | Sometimes you may need additional context to set the initial value of your store. To do this, there 25 | are a couple options. 26 | 27 | You can set the value at the beginning of your application, before your app renders (like in your 28 | main function). 29 | 30 | ```rust 31 | # extern crate yewdux; 32 | # use yewdux::prelude::*; 33 | # #[derive(PartialEq, Store, Default)] 34 | # struct MyStore { 35 | # foo: String, 36 | # bar: String, 37 | # } 38 | fn main() { 39 | // Construct foo and bar however necessary 40 | let foo = "foo".to_string(); 41 | let bar = "bar".to_string(); 42 | // Run this before starting your app. 43 | Dispatch::::global().set(MyStore { foo, bar }); 44 | // ... continue with your app setup 45 | } 46 | ``` 47 | 48 | You can also set the inital value from a function component. The `use_effect_with` hook can be used 49 | to run the hook only once (just be sure to use empty deps). 50 | 51 | ```rust 52 | # extern crate yew; 53 | # extern crate yewdux; 54 | # 55 | # use yewdux::prelude::*; 56 | # use yew::prelude::*; 57 | # #[derive(PartialEq, Store, Default)] 58 | # struct MyStore { 59 | # foo: String, 60 | # bar: String, 61 | # } 62 | #[function_component] 63 | fn MyComponent() -> Html { 64 | let dispatch = use_dispatch::(); 65 | // This runs only once, on the first render of the component. 66 | use_effect_with( 67 | (), // empty deps 68 | move |_| { 69 | // Construct foo and bar however necessary 70 | let foo = "foo".to_string(); 71 | let bar = "bar".to_string(); 72 | dispatch.set(MyStore { foo, bar }); 73 | || {} 74 | }, 75 | ); 76 | 77 | html! { 78 | // Your component html 79 | } 80 | } 81 | ``` 82 | 83 | Keep in mind your store will still be initialized with `Store::new` (usually that's set to 84 | `Default::default()`), however this is typically inexpensive. 85 | -------------------------------------------------------------------------------- /docs/src/derived_state.md: -------------------------------------------------------------------------------- 1 | # Derived State 2 | 3 | Derived state allows you to create state that automatically reacts to changes in another store. This is useful for: 4 | 5 | - Computing derived values from your primary state 6 | - Creating focused views of larger state objects 7 | - Building dependent state relationships 8 | 9 | ## Defining Derived State 10 | 11 | There are two ways to create derived state: 12 | 13 | 1. Using the `Store` macro with `derived_from` or `derived_from_mut` attributes 14 | 2. Manually implementing the `Store` trait and calling `derived_from` or `derived_from_mut` 15 | 16 | ### Using the Store Macro 17 | 18 | The simplest approach is to use the `Store` derive macro with the `derived_from` or `derived_from_mut` attributes: 19 | 20 | ```rust 21 | use std::rc::Rc; 22 | use yewdux::prelude::*; 23 | 24 | // Original source state 25 | #[derive(Default, Clone, PartialEq, Store)] 26 | struct Count { 27 | count: u32, 28 | } 29 | 30 | // Immutable derived state - creates a new instance on change 31 | #[derive(Default, Clone, PartialEq, Store)] 32 | #[store(derived_from(Count))] 33 | struct CountMultiplied { 34 | value: u32, 35 | } 36 | 37 | impl DerivedFrom for CountMultiplied { 38 | fn on_change(&self, state: Rc) -> Self { 39 | Self { 40 | value: state.count * 10, 41 | } 42 | } 43 | } 44 | 45 | // Mutable derived state - updates in place 46 | #[derive(Default, Clone, PartialEq, Store)] 47 | #[store(derived_from_mut(Count))] 48 | struct CountIsEven { 49 | status: bool, 50 | } 51 | 52 | impl DerivedFromMut for CountIsEven { 53 | fn on_change(&mut self, state: Rc) { 54 | self.status = state.count % 2 == 0; 55 | } 56 | } 57 | ``` 58 | 59 | ### Manual Implementation 60 | 61 | For more control, you can implement `Store` manually and register the relationship in your `new` method: 62 | 63 | ```rust 64 | #[derive(Default, Clone, PartialEq)] 65 | struct CountIsEven { 66 | status: bool, 67 | } 68 | 69 | impl DerivedFromMut for CountIsEven { 70 | fn on_change(&mut self, state: Rc) { 71 | self.status = state.count % 2 == 0; 72 | } 73 | } 74 | 75 | impl Store for CountIsEven { 76 | fn new(cx: &yewdux::Context) -> Self { 77 | // Register this state as derived from `Count` 78 | cx.derived_from_mut::(); 79 | 80 | // Initialize with current Count value 81 | let status = cx.get::().count % 2 == 0; 82 | Self { status } 83 | } 84 | 85 | fn should_notify(&self, old: &Self) -> bool { 86 | self != old 87 | } 88 | } 89 | ``` 90 | 91 | ## Using Derived State 92 | 93 | Using derived state is identical to using any other store: 94 | 95 | ```rust 96 | #[function_component] 97 | fn App() -> Html { 98 | let (count, dispatch) = use_store::(); 99 | let is_even = use_store_value::(); 100 | let multiplied = use_store_value::(); 101 | 102 | let onclick = dispatch.reduce_mut_callback(|state| state.count += 1); 103 | 104 | html! { 105 | <> 106 |

{"Count: "}{ count.count }

107 |

{"Is Even: "}{ is_even.status.to_string() }

108 |

{"Multiplied by 10: "}{ multiplied.value }

109 | 110 | 111 | } 112 | } 113 | ``` 114 | 115 | ## How It Works 116 | 117 | When you use `derived_from` or `derived_from_mut`: 118 | 119 | 1. A listener is registered that watches for changes in the source state 120 | 2. When the source state changes, your `on_change` implementation is called 121 | 3. Your derived state is updated either by creating a new instance (`DerivedFrom`) or by modifying it in place (`DerivedFromMut`) 122 | 4. Components using the derived state are re-rendered 123 | 124 | This provides a clean, type-safe way to create computed or dependent state without manual synchronization. -------------------------------------------------------------------------------- /docs/src/dispatch.md: -------------------------------------------------------------------------------- 1 | # Creating a dispatch 2 | 3 | A [Dispatch](https://docs.rs/yewdux/latest/yewdux/dispatch/struct.Dispatch.html) is the primary 4 | interface to access your [Store](https://docs.rs/yewdux/latest/yewdux/store/trait.Store.html). It 5 | can be used to read and write changes to state in various ways. 6 | 7 | ## Hooks 8 | 9 | A dispatch is provided when using the functional hook, which is only available in yew functional 10 | components. 11 | 12 | **IMPORTANT**: Like other hooks, all yewdux hooks must be used at the top level of a function 13 | component. 14 | 15 | ```rust 16 | # extern crate yewdux; 17 | # extern crate yew; 18 | # use yewdux::prelude::*; 19 | # use yew::prelude::*; 20 | #[derive(Default, PartialEq, Store)] 21 | struct State { 22 | count: u32, 23 | } 24 | 25 | #[function_component] 26 | fn MyComponent() -> Html { 27 | let (state, dispatch) = use_store::(); 28 | html! { 29 | // Component stuff here 30 | } 31 | } 32 | ``` 33 | 34 | See [the docs](https://docs.rs/yewdux/latest/yewdux/functional/index.html) for a full list of 35 | available hooks. 36 | 37 | ## Manually 38 | 39 | To create a dispatch, you need only provide the desired store type. This is available in **any** 40 | rust code, not just yew components. 41 | 42 | ```rust 43 | # extern crate yewdux; 44 | # use yewdux::prelude::*; 45 | # #[derive(Default, PartialEq, Store)] 46 | # struct State { 47 | # count: u32, 48 | # } 49 | let dispatch = Dispatch::::global(); 50 | ``` 51 | 52 | **NOTE**: Here we create a global dispatch, which is only available for wasm targets. See 53 | [SSR support](./ssr.md) for alternatives. 54 | 55 | # Changing state 56 | 57 | `Dispatch` provides many options for changing state. Here are a few handy methods. For a full list 58 | see the [docs](https://docs.rs/yewdux/latest/yewdux/dispatch/struct.Dispatch.html#) 59 | 60 | 61 | ```rust 62 | # extern crate yewdux; 63 | # extern crate yew; 64 | # use yewdux::prelude::*; 65 | # use yew::prelude::*; 66 | #[derive(Default, PartialEq, Store)] 67 | struct State { 68 | count: u32, 69 | } 70 | 71 | // Create a global dispatch 72 | let dispatch = Dispatch::::global(); 73 | 74 | // Set the value immediately 75 | dispatch.set(State { count: 0 }); 76 | 77 | // Set the value immediately based on the last value 78 | dispatch.reduce(|state| State { count: state.count + 1}.into()); 79 | 80 | // Create a callback to set the value when a button is clicked 81 | let onclick = dispatch.reduce_callback(|state| State { count: state.count + 1}.into()); 82 | html! { 83 | 84 | }; 85 | ``` 86 | 87 | ## Mut reducers 88 | 89 | There are `_mut` variants to every reducer function. This way has less boilerplate, and requires 90 | your `Store` to implement `Clone`. Your `Store` *may* be cloned once per mutation, 91 | 92 | ```rust 93 | # extern crate yewdux; 94 | # extern crate yew; 95 | # use yewdux::prelude::*; 96 | # use yew::prelude::*; 97 | #[derive(Default, PartialEq, Clone, Store)] 98 | struct State { 99 | count: u32, 100 | } 101 | 102 | // Create a global dispatch 103 | let dispatch = Dispatch::::global(); 104 | 105 | // Mutate the current value 106 | dispatch.reduce_mut(|state| state.count += 1); 107 | 108 | // Create a callback to mutate the value when a button is clicked 109 | let onclick = dispatch.reduce_mut_callback(|counter| counter.count += 1); 110 | html! { 111 | 112 | }; 113 | ``` 114 | 115 | ## Predictable mutations 116 | 117 | Yewdux supports predictable mutation. Simply define your message and apply it. 118 | 119 | ```rust 120 | # extern crate yewdux; 121 | # extern crate yew; 122 | use std::rc::Rc; 123 | 124 | use yew::prelude::*; 125 | use yewdux::prelude::*; 126 | 127 | #[derive(Default, PartialEq, Clone, Store)] 128 | struct State { 129 | count: u32, 130 | } 131 | 132 | enum Msg { 133 | AddOne, 134 | } 135 | 136 | impl Reducer for Msg { 137 | fn apply(self, state: Rc) -> Rc { 138 | match self { 139 | Msg::AddOne => State { count: state.count + 1 }.into(), 140 | } 141 | } 142 | } 143 | 144 | let dispatch = Dispatch::::global(); 145 | 146 | dispatch.apply(Msg::AddOne); 147 | 148 | let onclick = dispatch.apply_callback(|_| Msg::AddOne); 149 | html! { 150 | 151 | }; 152 | ``` 153 | 154 | ### Tip 155 | 156 | `Rc::make_mut` is handy if you prefer CoW: 157 | 158 | ```rust 159 | # extern crate yewdux; 160 | # use std::rc::Rc; 161 | # use yewdux::prelude::*; 162 | # #[derive(Default, PartialEq, Clone, Store)] 163 | # struct State { 164 | # count: u32, 165 | # } 166 | # enum Msg { 167 | # AddOne, 168 | # } 169 | impl Reducer for Msg { 170 | fn apply(self, mut state: Rc) -> Rc { 171 | let state_mut = Rc::make_mut(&mut state); 172 | 173 | match self { 174 | Msg::AddOne => state_mut.count += 1, 175 | }; 176 | 177 | state 178 | } 179 | } 180 | ``` 181 | 182 | ## Future support 183 | 184 | Because a `Dispatch` may be created and executed from anywhere, Yewdux has innate future support. 185 | Just use it normally, no additonal setup is needed. 186 | 187 | ```rust 188 | # extern crate yewdux; 189 | # extern crate yew; 190 | # use std::rc::Rc; 191 | # use yewdux::prelude::*; 192 | # use yew::prelude::*; 193 | 194 | #[derive(Default, PartialEq, Store)] 195 | struct User { 196 | name: Option>, 197 | } 198 | 199 | async fn get_user() -> User { 200 | User { name: Some("bob".into()) } 201 | } 202 | 203 | let dispatch = Dispatch::::global(); 204 | // Use yew::platform::spawn_local to run a future. 205 | let future = async move { 206 | let user = get_user().await; 207 | dispatch.set(user); 208 | }; 209 | ``` 210 | 211 | -------------------------------------------------------------------------------- /docs/src/example.md: -------------------------------------------------------------------------------- 1 | # Quickstart example 2 | 3 | Below you'll find a simple counter example, demonstrating how to read and write to shared state. 4 | 5 | ```rust 6 | # extern crate yewdux; 7 | # extern crate yew; 8 | use yew::prelude::*; 9 | use yewdux::prelude::*; 10 | 11 | #[derive(Default, Clone, PartialEq, Store)] 12 | struct State { 13 | count: u32, 14 | } 15 | 16 | #[function_component] 17 | fn ViewCount() -> Html { 18 | let (state, _) = use_store::(); 19 | html!(state.count) 20 | } 21 | 22 | #[function_component] 23 | fn IncrementCount() -> Html { 24 | let (_, dispatch) = use_store::(); 25 | let onclick = dispatch.reduce_mut_callback(|counter| counter.count += 1); 26 | 27 | html! { 28 | 29 | } 30 | } 31 | 32 | #[function_component] 33 | fn App() -> Html { 34 | html! { 35 | <> 36 | 37 | 38 | 39 | } 40 | } 41 | ``` 42 | 43 | ## Additional examples 44 | 45 | Complete working examples can be found in the 46 | [examples](https://github.com/intendednull/yewdux/tree/master/examples) folder of github. 47 | 48 | To run an example you'll need to install [trunk](https://github.com/thedodd/trunk) (a rust wasm 49 | bundler), then run the following command (replacing [example] with your desired example name): 50 | ```bash 51 | trunk serve examples/[example]/index.html --open 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/src/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | A state management solution for the [Yew](https://yew.rs) front-end library. 4 | 5 | This crate was inspired by [Redux](https://redux.js.org/), however some deviation was taken in 6 | the spirit of Rust. 7 | 8 | This book is currently in development. If it is confusing in any way, or you have suggestions, 9 | please post an issue in the [repo](https://github.com/intendednull/yewdux) or ask in the 10 | [Yew discord](https://discord.gg/UmS6FKYa5a). 11 | 12 | ## Why Yewdux? 13 | 14 | State management in Yew can be difficult. Especially when many different components need access to 15 | the same state. Properties and callbacks work great for simple relationships, however quickly become 16 | cumbersome when you need to propagate state through many (potentially isolated) layers of 17 | components. Yew's [context manager](https://yew.rs/docs/concepts/contexts) does a decent job, and is 18 | worth serious consideration, however it requires substantial boilerplate and is not that easy to 19 | use. 20 | 21 | This crate aims to provide a dead-simple, ergonomic approach to global state management. It 22 | encourages modular state by providing easy setup and access to your shared state, allowing you to 23 | write cleaner code while remaining productive. 24 | 25 | It does **not** try to provide any additional patterns or features which aren't directly related to 26 | accessing or manipulating shared state. 27 | 28 | Yewdux was built with the following goals: 29 | 30 | - **Simple** - the only required trait is [Store](./store.md). 31 | - **Ergonomic** - boilerplate is optional! 32 | - **Predictable** - you have complete control over how state is changed. 33 | - **Selective** - only render when you need to (see [selectors](./reading.md#selectors)). 34 | - **Context agnostic** - you can create and execute a [dispatch](./dispatch.md) from anywhere. 35 | - **Complete component support** - compatible with both functional and struct components. 36 | 37 | ## Alternatives 38 | 39 | - [Bounce](https://github.com/bounce-rs/bounce) - The uncomplicated Yew State management library 40 | -------------------------------------------------------------------------------- /docs/src/listeners.md: -------------------------------------------------------------------------------- 1 | # Listeners 2 | 3 | Listeners are component-less subscribers. They are used to describe side-effects that should happen 4 | whenever state changes. They live for application lifetime, and are created with `init_listener`. 5 | 6 | Here's a simple listener that logs the current state whenever it changes. 7 | ```rust 8 | # extern crate yew; 9 | # extern crate yewdux; 10 | # use yew::prelude::*; 11 | use std::rc::Rc; 12 | 13 | use yewdux::prelude::*; 14 | 15 | #[derive(Default, Clone, PartialEq, Debug, Store)] 16 | struct State { 17 | count: u32, 18 | } 19 | 20 | struct StateLogger; 21 | impl Listener for StateLogger { 22 | // Here's where we define which store we are listening to. 23 | type Store = State; 24 | // Here's where we decide what happens when `State` changes. 25 | fn on_change(&self, _cx: &yewdux::Context, state: Rc) { 26 | yewdux::log::info!("state changed: {:?}", state); 27 | } 28 | } 29 | ``` 30 | 31 | Can can start the listener by calling `init_listener` somewhere in our code. A good place to put it is 32 | the store constructor. 33 | 34 | **NOTE**: Successive calls to `init_listener` on the same type will do nothing. 35 | 36 | ```rust 37 | # extern crate yewdux; 38 | # use std::rc::Rc; 39 | # use yewdux::prelude::*; 40 | # #[derive(Default, PartialEq, Debug)] 41 | # struct State { 42 | # count: u32, 43 | # } 44 | # struct StateLogger; 45 | # impl Listener for StateLogger { 46 | # // Here's where we say which store we want to subscribe to. 47 | # type Store = State; 48 | # 49 | # fn on_change(&self, _cx: &yewdux::Context, state: Rc) { 50 | # yewdux::log::info!("state changed: {:?}", state); 51 | # } 52 | # } 53 | 54 | impl Store for State { 55 | fn new(cx: &yewdux::Context) -> Self { 56 | init_listener(|| StateLogger, cx); 57 | Default::default() 58 | } 59 | 60 | fn should_notify(&self, other: &Self) -> bool { 61 | self != other 62 | } 63 | } 64 | ``` 65 | 66 | ## Tracking state 67 | 68 | Sometimes it's useful to keep track of how a store has been changing over time. However this should 69 | not be done in the listener itself. Notice `Listener::on_change` takes an immutable reference. This 70 | is necessary because otherwise we start to run into borrowing issues when listeners are triggered 71 | recursively. 72 | 73 | To track changes we can instead use a separate store that listens to the store we want to track. 74 | 75 | ```rust 76 | # extern crate yewdux; 77 | # use std::rc::Rc; 78 | # use yewdux::prelude::*; 79 | # #[derive(Default, PartialEq, Debug)] 80 | # struct State { 81 | # count: u32, 82 | # } 83 | 84 | #[derive(Default, PartialEq, Debug, Store, Clone)] 85 | struct ChangeTracker { 86 | count: u32, 87 | } 88 | 89 | struct ChangeTrackerListener; 90 | impl Listener for StateLogger { 91 | type Store = State; 92 | 93 | fn on_change(&self, cx: &yewdux::Context, state: Rc) { 94 | let dispatch = Dispatch::::new(cx); 95 | dipatch.reduce_mut(|state| state.count += 1); 96 | let count = dispatch.get().count; 97 | println!("State has changed {} times", count); 98 | } 99 | } 100 | 101 | impl Store for State { 102 | fn new(cx: &yewdux::Context) -> Self { 103 | init_listener(|| ChangeTrackerListener, cx); 104 | Default::default() 105 | } 106 | 107 | fn should_notify(&self, other: &Self) -> bool { 108 | self != other 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /docs/src/persistence.md: -------------------------------------------------------------------------------- 1 | # Persistence 2 | 3 | Yewdux provides the `#[store]` macro to easily persist your state in either local or session storage. 4 | 5 | ```rust 6 | # extern crate yewdux; 7 | # extern crate serde; 8 | use yewdux::prelude::*; 9 | use serde::{Serialize, Deserialize}; 10 | 11 | #[derive(Default, PartialEq, Serialize, Deserialize, Store)] 12 | #[store(storage = "local")] // can also be "session" 13 | struct State { 14 | count: u32, 15 | } 16 | ``` 17 | 18 | This can also be done 19 | [manually](https://github.com/intendednull/yewdux/blob/master/examples/listener/src/main.rs). 20 | 21 | ## Tab sync 22 | 23 | Normally if your application is open in multiple tabs, the store is not updated in any tab other 24 | than the current one. If you want storage to sync in all tabs, add `storage_tab_sync` to the macro. 25 | 26 | ```rust 27 | # extern crate yewdux; 28 | # extern crate serde; 29 | # use yewdux::prelude::*; 30 | # use serde::{Serialize, Deserialize}; 31 | #[derive(Default, Clone, PartialEq, Eq, Deserialize, Serialize, Store)] 32 | #[store(storage = "local", storage_tab_sync)] 33 | struct State { 34 | count: u32, 35 | } 36 | ``` 37 | 38 | ## Additional Listeners 39 | 40 | You can inject additional listeners into the `#[store]` macro. 41 | 42 | ```rust 43 | # extern crate yewdux; 44 | # extern crate serde; 45 | # use std::rc::Rc; 46 | # use yewdux::prelude::*; 47 | # use serde::{Serialize, Deserialize}; 48 | #[derive(Default, Clone, PartialEq, Eq, Deserialize, Serialize, Store)] 49 | #[store(storage = "local", listener(LogListener))] 50 | struct State { 51 | count: u32, 52 | } 53 | 54 | struct LogListener; 55 | impl Listener for LogListener { 56 | type Store = State; 57 | 58 | fn on_change(&mut self, _cx: &yewdux::Context, state: Rc) { 59 | yewdux::log::info!("Count changed to {}", state.count); 60 | } 61 | } 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /docs/src/reading.md: -------------------------------------------------------------------------------- 1 | # Reading state 2 | 3 | To get the current state of your store immediately, use `Dispatch::get`: 4 | 5 | **IMPORTANT**: Reading the state this way **does not** provide any sort of change detection, and 6 | your component **will not** automatically re-render when state changes. 7 | 8 | ```rust 9 | # extern crate yewdux; 10 | use std::rc::Rc; 11 | 12 | use yewdux::prelude::*; 13 | 14 | #[derive(PartialEq, Default, Store)] 15 | struct State { 16 | count: u32, 17 | } 18 | 19 | // Create a dispatch from the global context. This works for non-global contexts too, we would just 20 | // pass in the context we want. 21 | let dispatch = Dispatch::::global(); 22 | let state: Rc = dispatch.get(); 23 | ``` 24 | 25 | ## Subscribing to your store 26 | 27 | In order for your component to know when state changes, we need to subscribe. 28 | 29 | ### Function components 30 | 31 | The `use_store` hook automatically subscribes to your store, and re-renders when state changes. This 32 | **must** be called at the top level of your function component. 33 | 34 | ```rust 35 | # extern crate yewdux; 36 | # extern crate yew; 37 | # use yewdux::prelude::*; 38 | # use yew::prelude::*; 39 | # #[derive(PartialEq, Default, Store)] 40 | # struct State { 41 | # count: u32, 42 | # } 43 | #[function_component] 44 | fn ViewCount() -> Html { 45 | let (state, dispatch) = use_store::(); 46 | html!(state.count) 47 | } 48 | ``` 49 | 50 | ### Struct components 51 | 52 | For struct components we need to subscribe manually. This way allows much finer control, at the cost 53 | of extra boilerplate. 54 | 55 | **IMPORTANT**: Remember to hold onto your dispatch instance. Dropping it will drop the entire 56 | subscription, and you will **not** receive changes to state. 57 | 58 | ```rust 59 | # extern crate yewdux; 60 | # extern crate yew; 61 | use std::rc::Rc; 62 | 63 | use yew::prelude::*; 64 | use yewdux::prelude::*; 65 | 66 | #[derive(PartialEq, Default, Clone, Store)] 67 | struct State { 68 | count: u32, 69 | } 70 | 71 | struct MyComponent { 72 | dispatch: Dispatch, 73 | state: Rc, 74 | 75 | } 76 | 77 | enum Msg { 78 | StateChanged(Rc), 79 | } 80 | 81 | impl Component for MyComponent { 82 | type Properties = (); 83 | type Message = Msg; 84 | 85 | fn create(ctx: &Context) -> Self { 86 | // The callback for receiving updates to state. 87 | let callback = ctx.link().callback(Msg::StateChanged); 88 | // Subscribe to changes in state. New state is received in `update`. Be sure to save this, 89 | // dropping it will unsubscribe. 90 | let dispatch = Dispatch::::global().subscribe_silent(callback); 91 | Self { 92 | // Get the current state. 93 | state: dispatch.get(), 94 | dispatch, 95 | } 96 | } 97 | 98 | fn update(&mut self, ctx: &Context, msg: Msg) -> bool { 99 | match msg { 100 | // Receive new state. 101 | Msg::StateChanged(state) => { 102 | self.state = state; 103 | 104 | // Only re-render this component if count is greater that 0 (for this example). 105 | if self.state.count > 0 { 106 | true 107 | } else { 108 | false 109 | } 110 | } 111 | } 112 | } 113 | 114 | fn view(&self, ctx: &Context) -> Html { 115 | let count = self.state.count; 116 | let onclick = self.dispatch.reduce_mut_callback(|s| s.count += 1); 117 | html! { 118 | <> 119 |

{ count }

120 | 121 | 122 | } 123 | } 124 | 125 | } 126 | ``` 127 | 128 | # Selectors 129 | 130 | Sometimes a component will only care about a particular part of state, and only needs to re-render 131 | when that part changes. For this we have the `use_selector` hook. 132 | 133 | ```rust 134 | # extern crate yewdux; 135 | # extern crate yew; 136 | use yewdux::prelude::*; 137 | use yew::prelude::*; 138 | 139 | #[derive(Default, Clone, PartialEq, Store)] 140 | struct User { 141 | first_name: String, 142 | last_name: String, 143 | } 144 | 145 | #[function_component] 146 | fn DisplayFirst() -> Html { 147 | // This will only re-render when the first name has changed. It will **not** re-render if any 148 | // other field has changed. 149 | // 150 | // Note: we are cloning a string. Probably insignificant for this example, however 151 | // sometimes it may be beneficial to wrap fields that are expensive to clone in an `Rc`. 152 | let first_name = use_selector(|state: &User| state.first_name.clone()); 153 | 154 | html! { 155 |

{ first_name }

156 | } 157 | } 158 | ``` 159 | 160 | ## Capturing your environment 161 | 162 | For selectors that need to capture variables from their environment, be sure to provide them as 163 | dependencies to `use_selector_with_deps`. Otherwise your selector won't update correctly! 164 | 165 | ```rust 166 | # extern crate yewdux; 167 | # extern crate yew; 168 | use std::collections::HashMap; 169 | 170 | use yewdux::prelude::*; 171 | use yew::prelude::*; 172 | 173 | #[derive(Default, Clone, PartialEq, Store)] 174 | struct Items { 175 | inner: HashMap, 176 | } 177 | 178 | #[derive(Clone, PartialEq, Properties)] 179 | struct DisplayItemProps { 180 | item_id: u32, 181 | } 182 | 183 | #[function_component] 184 | fn DisplayItem(props: &DisplayItemProps) -> Html { 185 | // For multiple dependencies, try using a tuple: (dep1, dep2, ..) 186 | let item = use_selector_with_deps( 187 | |state: &Items, item_id| state.inner.get(item_id).cloned(), 188 | props.item_id, 189 | ); 190 | // Only render the item if it exists. 191 | let item = match item.as_ref() { 192 | Some(item) => item, 193 | None => return Default::default(), 194 | }; 195 | 196 | html! { 197 |

{ item }

198 | } 199 | } 200 | ``` 201 | -------------------------------------------------------------------------------- /docs/src/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Add Yewdux to your project's `Cargo.toml`. Make sure Yew has the "csr" feature (client side rendering): 4 | 5 | ### Stable release: 6 | 7 | ```toml 8 | [dependencies] 9 | yew = { version = "0.21", features = ["csr"] } 10 | yewdux = "0.10" 11 | ``` 12 | 13 | ### Development branch: 14 | 15 | ```toml 16 | [dependencies] 17 | yew = { git = "https://github.com/yewstack/yew.git", features = ["csr"] } 18 | yewdux = { git = "https://github.com/intendednull/yewdux.git" } 19 | ``` 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/src/ssr.md: -------------------------------------------------------------------------------- 1 | # SSR Support 2 | 3 | By default Yewdux uses a global `Context` that is shared thread-locally. This means we can share 4 | state from anywhere in our code as long as it's within the same thread. Wasm applications are 5 | strictly single threaded (without workers), so it isn't a problem. 6 | 7 | However the same cannot be said for server side rendering. It is very possible the server is 8 | executing in a multi-threaded environment, which could cause various problems for Yewdux's 9 | single-threaded assumption. 10 | 11 | While multi-threaded globally shared state is technically possible, it is currently not supported. 12 | 13 | Instead Yewdux offers a custom component to hold your shared application state: `YewduxRoot`. This 14 | ensures all state is kept inside your Yew app. 15 | 16 | ```rust 17 | # extern crate yew; 18 | # extern crate yewdux; 19 | use yew::prelude::*; 20 | use yewdux::prelude::*; 21 | 22 | #[derive(Default, Clone, PartialEq, Eq, Store)] 23 | struct State { 24 | count: u32, 25 | } 26 | 27 | #[function_component] 28 | fn Counter() -> Html { 29 | let (state, dispatch) = use_store::(); 30 | let onclick = dispatch.reduce_mut_callback(|state| state.count += 1); 31 | html! { 32 | <> 33 |

{ state.count }

34 | 35 | 36 | } 37 | } 38 | 39 | #[function_component] 40 | fn App() -> Html { 41 | // YewduxRoot must be kept above all components that use any of your stores. 42 | html! { 43 | 44 | 45 | 46 | } 47 | } 48 | ``` 49 | 50 | Yewdux hooks automatically detect when YewduxRoot is present, and use it accordingly. 51 | 52 | ## SSR with struct components 53 | 54 | For struct component support, refer to the [higher order components 55 | pattern](https://yew.rs/docs/advanced-topics/struct-components/hoc). 56 | 57 | ```rust 58 | # extern crate yew; 59 | # extern crate yewdux; 60 | use std::rc::Rc; 61 | 62 | use yew::prelude::*; 63 | use yewdux::prelude::*; 64 | 65 | #[derive(Default, Clone, PartialEq, Eq, Store)] 66 | struct State { 67 | count: u32, 68 | } 69 | 70 | #[derive(Properties, Clone, PartialEq)] 71 | struct Props { 72 | dispatch: Dispatch, 73 | } 74 | 75 | enum Msg { 76 | StateChanged(Rc), 77 | } 78 | 79 | struct MyComponent { 80 | state: Rc, 81 | dispatch: Dispatch, 82 | } 83 | 84 | impl Component for MyComponent { 85 | type Properties = Props; 86 | type Message = Msg; 87 | 88 | fn create(ctx: &Context) -> Self { 89 | let callback = ctx.link().callback(Msg::StateChanged); 90 | let dispatch = ctx.props().dispatch.clone().subscribe_silent(callback); 91 | Self { 92 | state: dispatch.get(), 93 | dispatch, 94 | } 95 | } 96 | 97 | fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { 98 | match msg { 99 | Msg::StateChanged(state) => { 100 | self.state = state; 101 | true 102 | } 103 | } 104 | } 105 | 106 | fn view(&self, ctx: &Context) -> Html { 107 | let count = self.state.count; 108 | let onclick = self.dispatch.reduce_mut_callback(|s| s.count += 1); 109 | html! { 110 | <> 111 |

{ count }

112 | 113 | 114 | } 115 | } 116 | 117 | } 118 | 119 | #[function_component] 120 | fn MyComponentHoc() -> Html { 121 | let dispatch = use_dispatch::(); 122 | 123 | html! { 124 | 125 | } 126 | } 127 | 128 | 129 | #[function_component] 130 | fn App() -> Html { 131 | // YewduxRoot must be kept above all components that use any of your stores. 132 | html! { 133 | 134 | 135 | 136 | } 137 | } 138 | ``` 139 | -------------------------------------------------------------------------------- /docs/src/store.md: -------------------------------------------------------------------------------- 1 | # Defining a Store 2 | 3 | A [Store](https://docs.rs/yewdux/0.8.1/yewdux/store/trait.Store.html) represents state that is 4 | shared application-wide. It is initialized on first access, and lives for application lifetime. 5 | 6 | Implement `Store` for your state using the macro. 7 | 8 | ```rust 9 | # extern crate yewdux; 10 | use yewdux::prelude::*; 11 | 12 | #[derive(Default, PartialEq, Store)] 13 | struct State { 14 | count: u32, 15 | } 16 | ``` 17 | 18 | ## Store Attributes 19 | 20 | The `Store` derive macro supports several attributes to customize behavior: 21 | 22 | ```rust 23 | #[derive(Default, PartialEq, Store)] 24 | #[store(storage = "local")] // Enable local storage persistence 25 | #[store(storage_tab_sync = true)] // Enable tab synchronization 26 | #[store(listener(MyCustomListener))] // Register custom listeners 27 | #[store(derived_from(OtherStore))] // Create derived state (immutable) 28 | #[store(derived_from_mut(OtherStore))] // Create derived state (mutable) 29 | struct State { 30 | count: u32, 31 | } 32 | ``` 33 | 34 | ## Manual Implementation 35 | 36 | It is also simple to define a `Store` manually. This is useful when you need finer control over how 37 | it is created, or when to notify components. 38 | 39 | ```rust 40 | # extern crate yewdux; 41 | # use yewdux::prelude::*; 42 | #[derive(PartialEq)] 43 | struct State { 44 | count: u32, 45 | } 46 | 47 | impl Store for State { 48 | fn new(_cx: &yewdux::Context) -> Self { 49 | Self { 50 | count: Default::default(), 51 | } 52 | } 53 | 54 | fn should_notify(&self, old: &Self) -> bool { 55 | // When this returns true, all components are notified and consequently re-render. 56 | self != old 57 | } 58 | } 59 | ``` 60 | 61 | *Note: implementing `Store` doesn't require any additional traits, however `Default` and 62 | `PartialEq` are required for the macro.* 63 | 64 | See [Derived State](./derived_state.md) for more information on creating stores that automatically update in response to changes in other stores. 65 | -------------------------------------------------------------------------------- /examples/async_proxy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async_proxy" 3 | version = "0.1.0" 4 | authors = ["Alister Lee "] 5 | edition = "2018" 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | wasm-bindgen-futures = "0.4.30" 10 | yew = { git = "https://github.com/yewstack/yew.git", features = ["csr"] } 11 | yewdux = { path = "../../crates/yewdux" } 12 | reqwasm = "0.5.0" 13 | serde = "1.0.140" 14 | serde_json = "1.0.82" 15 | web-sys = "0.3.59" -------------------------------------------------------------------------------- /examples/async_proxy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/async_proxy/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_arch = "wasm32")] 2 | 3 | use yew::prelude::*; 4 | use yewdux::prelude::*; 5 | 6 | mod proxy; 7 | 8 | use proxy::State; 9 | 10 | #[function_component] 11 | fn App() -> Html { 12 | let state = use_store_value::(); 13 | let timezones = state 14 | .timezones() 15 | .map(|timezone| { 16 | html! {