Introduction
A state management solution for the Yew front-end library.
This crate was inspired by Redux, however some deviation was taken in the spirit of Rust.
This book is currently in development. If it is confusing in any way, or you have suggestions, please post an issue in the repo or ask in the Yew discord.
Why Yewdux?
State management in Yew can be difficult. Especially when many different components need access to the same state. Properties and callbacks work great for simple relationships, however quickly become cumbersome when you need to propagate state through many (potentially isolated) layers of components. Yew's context manager does a decent job, and is worth serious consideration, however it requires substantial boilerplate and is not that easy to use.
This crate aims to provide a dead-simple, ergonomic approach to global state management. It encourages modular state by providing easy setup and access to your shared state, allowing you to write cleaner code while remaining productive.
It does not try to provide any additional patterns or features which aren't directly related to accessing or manipulating shared state.
Yewdux was built with the following goals:
- Simple - the only required trait is Store.
- Ergonomic - boilerplate is optional!
- Predictable - you have complete control over how state is changed.
- Selective - only render when you need to (see selectors).
- Context agnostic - you can create and execute a dispatch from anywhere.
- Complete component support - compatible with both functional and struct components.
Alternatives
- Bounce - The uncomplicated Yew State management library
Setup
Add Yewdux to your project's Cargo.toml
. Make sure Yew has the "csr" feature (client side rendering):
Stable release:
[dependencies]
yew = { version = "0.21", features = ["csr"] }
yewdux = "0.10"
Development branch:
[dependencies]
yew = { git = "https://github.com/yewstack/yew.git", features = ["csr"] }
yewdux = { git = "https://github.com/intendednull/yewdux.git" }
Quickstart example
Below you'll find a simple counter example, demonstrating how to read and write to shared state.
#![allow(unused)] fn main() { extern crate yewdux; extern crate yew; use yew::prelude::*; use yewdux::prelude::*; #[derive(Default, Clone, PartialEq, Store)] struct State { count: u32, } #[function_component] fn ViewCount() -> Html { let (state, _) = use_store::<State>(); html!(state.count) } #[function_component] fn IncrementCount() -> Html { let (_, dispatch) = use_store::<State>(); let onclick = dispatch.reduce_mut_callback(|counter| counter.count += 1); html! { <button {onclick}>{"+1"}</button> } } #[function_component] fn App() -> Html { html! { <> <ViewCount /> <IncrementCount /> </> } } }
Additional examples
Complete working examples can be found in the examples folder of github.
To run an example you'll need to install trunk (a rust wasm bundler), then run the following command (replacing [example] with your desired example name):
trunk serve examples/[example]/index.html --open
Defining a Store
A Store represents state that is shared application-wide. It is initialized on first access, and lives for application lifetime.
Implement Store
for your state using the macro.
#![allow(unused)] fn main() { extern crate yewdux; use yewdux::prelude::*; #[derive(Default, PartialEq, Store)] struct State { count: u32, } }
It is also simple to define a Store
manually. This is useful when you need finer control over how
it is created, or when to notify components.
#![allow(unused)] fn main() { extern crate yewdux; use yewdux::prelude::*; #[derive(PartialEq)] struct State { count: u32, } impl Store for State { fn new(_cx: &yewdux::Context) -> Self { Self { count: Default::default(), } } fn should_notify(&self, old: &Self) -> bool { // When this returns true, all components are notified and consequently re-render. self != old } } }
Note: implementing Store
doesn't require any additional traits, however Default
and
PartialEq
are required for the macro.
Setting default store values
The best way to define the default value of your store is by manually implementing Default
.
#![allow(unused)] fn main() { extern crate yewdux; use yewdux::prelude::*; #[derive(PartialEq, Store)] struct MyStore { foo: String, bar: String, } impl Default for MyStore { fn default() -> Self { Self { foo: "foo".to_string(), bar: "bar".to_string(), } } } }
Sometimes you may need additional context to set the initial value of your store. To do this, there are a couple options.
You can set the value at the beginning of your application, before your app renders (like in your main function).
extern crate yewdux; use yewdux::prelude::*; #[derive(PartialEq, Store, Default)] struct MyStore { foo: String, bar: String, } fn main() { // Construct foo and bar however necessary let foo = "foo".to_string(); let bar = "bar".to_string(); // Run this before starting your app. Dispatch::<MyStore>::global().set(MyStore { foo, bar }); // ... continue with your app setup }
You can also set the inital value from a function component. The use_effect_with
hook can be used
to run the hook only once (just be sure to use empty deps).
#![allow(unused)] fn main() { extern crate yew; extern crate yewdux; use yewdux::prelude::*; use yew::prelude::*; #[derive(PartialEq, Store, Default)] struct MyStore { foo: String, bar: String, } #[function_component] fn MyComponent() -> Html { let dispatch = use_dispatch::<MyStore>(); // This runs only once, on the first render of the component. use_effect_with( (), // empty deps move |_| { // Construct foo and bar however necessary let foo = "foo".to_string(); let bar = "bar".to_string(); dispatch.set(MyStore { foo, bar }); || {} }, ); html! { // Your component html } } }
Keep in mind your store will still be initialized with Store::new
(usually that's set to
Default::default()
), however this is typically inexpensive.
Persistence
Yewdux provides the #[store]
macro to easily persist your state in either local or session storage.
#![allow(unused)] fn main() { extern crate yewdux; extern crate serde; use yewdux::prelude::*; use serde::{Serialize, Deserialize}; #[derive(Default, PartialEq, Serialize, Deserialize, Store)] #[store(storage = "local")] // can also be "session" struct State { count: u32, } }
This can also be done manually.
Tab sync
Normally if your application is open in multiple tabs, the store is not updated in any tab other
than the current one. If you want storage to sync in all tabs, add storage_tab_sync
to the macro.
#![allow(unused)] fn main() { extern crate yewdux; extern crate serde; use yewdux::prelude::*; use serde::{Serialize, Deserialize}; #[derive(Default, Clone, PartialEq, Eq, Deserialize, Serialize, Store)] #[store(storage = "local", storage_tab_sync)] struct State { count: u32, } }
Additional Listeners
You can inject additional listeners into the #[store]
macro.
#![allow(unused)] fn main() { extern crate yewdux; extern crate serde; use std::rc::Rc; use yewdux::prelude::*; use serde::{Serialize, Deserialize}; #[derive(Default, Clone, PartialEq, Eq, Deserialize, Serialize, Store)] #[store(storage = "local", listener(LogListener))] struct State { count: u32, } struct LogListener; impl Listener for LogListener { type Store = State; fn on_change(&mut self, _cx: &yewdux::Context, state: Rc<Self::Store>) { yewdux::log::info!("Count changed to {}", state.count); } } }
Creating a dispatch
A Dispatch is the primary interface to access your Store. It can be used to read and write changes to state in various ways.
Hooks
A dispatch is provided when using the functional hook, which is only available in yew functional components.
IMPORTANT: Like other hooks, all yewdux hooks must be used at the top level of a function component.
#![allow(unused)] fn main() { extern crate yewdux; extern crate yew; use yewdux::prelude::*; use yew::prelude::*; #[derive(Default, PartialEq, Store)] struct State { count: u32, } #[function_component] fn MyComponent() -> Html { let (state, dispatch) = use_store::<State>(); html! { // Component stuff here } } }
See the docs for a full list of available hooks.
Manually
To create a dispatch, you need only provide the desired store type. This is available in any rust code, not just yew components.
#![allow(unused)] fn main() { extern crate yewdux; use yewdux::prelude::*; #[derive(Default, PartialEq, Store)] struct State { count: u32, } let dispatch = Dispatch::<State>::global(); }
NOTE: Here we create a global dispatch, which is only available for wasm targets. See SSR support for alternatives.
Changing state
Dispatch
provides many options for changing state. Here are a few handy methods. For a full list
see the docs
#![allow(unused)] fn main() { extern crate yewdux; extern crate yew; use yewdux::prelude::*; use yew::prelude::*; #[derive(Default, PartialEq, Store)] struct State { count: u32, } // Create a global dispatch let dispatch = Dispatch::<State>::global(); // Set the value immediately dispatch.set(State { count: 0 }); // Set the value immediately based on the last value dispatch.reduce(|state| State { count: state.count + 1}.into()); // Create a callback to set the value when a button is clicked let onclick = dispatch.reduce_callback(|state| State { count: state.count + 1}.into()); html! { <button {onclick}>{"Increment (+1)"}</button> }; }
Mut reducers
There are _mut
variants to every reducer function. This way has less boilerplate, and requires
your Store
to implement Clone
. Your Store
may be cloned once per mutation,
#![allow(unused)] fn main() { extern crate yewdux; extern crate yew; use yewdux::prelude::*; use yew::prelude::*; #[derive(Default, PartialEq, Clone, Store)] struct State { count: u32, } // Create a global dispatch let dispatch = Dispatch::<State>::global(); // Mutate the current value dispatch.reduce_mut(|state| state.count += 1); // Create a callback to mutate the value when a button is clicked let onclick = dispatch.reduce_mut_callback(|counter| counter.count += 1); html! { <button {onclick}>{"Increment (+1)"}</button> }; }
Predictable mutations
Yewdux supports predictable mutation. Simply define your message and apply it.
#![allow(unused)] fn main() { extern crate yewdux; extern crate yew; use std::rc::Rc; use yew::prelude::*; use yewdux::prelude::*; #[derive(Default, PartialEq, Clone, Store)] struct State { count: u32, } enum Msg { AddOne, } impl Reducer<State> for Msg { fn apply(self, state: Rc<State>) -> Rc<State> { match self { Msg::AddOne => State { count: state.count + 1 }.into(), } } } let dispatch = Dispatch::<State>::global(); dispatch.apply(Msg::AddOne); let onclick = dispatch.apply_callback(|_| Msg::AddOne); html! { <button {onclick}>{"Increment (+1)"}</button> }; }
Tip
Rc::make_mut
is handy if you prefer CoW:
#![allow(unused)] fn main() { extern crate yewdux; use std::rc::Rc; use yewdux::prelude::*; #[derive(Default, PartialEq, Clone, Store)] struct State { count: u32, } enum Msg { AddOne, } impl Reducer<State> for Msg { fn apply(self, mut state: Rc<State>) -> Rc<State> { let state_mut = Rc::make_mut(&mut state); match self { Msg::AddOne => state_mut.count += 1, }; state } } }
Future support
Because a Dispatch
may be created and executed from anywhere, Yewdux has innate future support.
Just use it normally, no additonal setup is needed.
#![allow(unused)] fn main() { extern crate yewdux; extern crate yew; use std::rc::Rc; use yewdux::prelude::*; use yew::prelude::*; #[derive(Default, PartialEq, Store)] struct User { name: Option<Rc<str>>, } async fn get_user() -> User { User { name: Some("bob".into()) } } let dispatch = Dispatch::<User>::global(); // Use yew::platform::spawn_local to run a future. let future = async move { let user = get_user().await; dispatch.set(user); }; }
Reading state
To get the current state of your store immediately, use Dispatch::get
:
IMPORTANT: Reading the state this way does not provide any sort of change detection, and your component will not automatically re-render when state changes.
#![allow(unused)] fn main() { extern crate yewdux; use std::rc::Rc; use yewdux::prelude::*; #[derive(PartialEq, Default, Store)] struct State { count: u32, } // Create a dispatch from the global context. This works for non-global contexts too, we would just // pass in the context we want. let dispatch = Dispatch::<State>::global(); let state: Rc<State> = dispatch.get(); }
Subscribing to your store
In order for your component to know when state changes, we need to subscribe.
Function components
The use_store
hook automatically subscribes to your store, and re-renders when state changes. This
must be called at the top level of your function component.
#![allow(unused)] fn main() { extern crate yewdux; extern crate yew; use yewdux::prelude::*; use yew::prelude::*; #[derive(PartialEq, Default, Store)] struct State { count: u32, } #[function_component] fn ViewCount() -> Html { let (state, dispatch) = use_store::<State>(); html!(state.count) } }
Struct components
For struct components we need to subscribe manually. This way allows much finer control, at the cost of extra boilerplate.
IMPORTANT: Remember to hold onto your dispatch instance. Dropping it will drop the entire subscription, and you will not receive changes to state.
#![allow(unused)] fn main() { extern crate yewdux; extern crate yew; use std::rc::Rc; use yew::prelude::*; use yewdux::prelude::*; #[derive(PartialEq, Default, Clone, Store)] struct State { count: u32, } struct MyComponent { dispatch: Dispatch<State>, state: Rc<State>, } enum Msg { StateChanged(Rc<State>), } impl Component for MyComponent { type Properties = (); type Message = Msg; fn create(ctx: &Context<Self>) -> Self { // The callback for receiving updates to state. let callback = ctx.link().callback(Msg::StateChanged); // Subscribe to changes in state. New state is received in `update`. Be sure to save this, // dropping it will unsubscribe. let dispatch = Dispatch::<State>::global().subscribe_silent(callback); Self { // Get the current state. state: dispatch.get(), dispatch, } } fn update(&mut self, ctx: &Context<Self>, msg: Msg) -> bool { match msg { // Receive new state. Msg::StateChanged(state) => { self.state = state; // Only re-render this component if count is greater that 0 (for this example). if self.state.count > 0 { true } else { false } } } } fn view(&self, ctx: &Context<Self>) -> Html { let count = self.state.count; let onclick = self.dispatch.reduce_mut_callback(|s| s.count += 1); html! { <> <h1>{ count }</h1> <button onclick={onclick}>{"+1"}</button> </> } } } }
Selectors
Sometimes a component will only care about a particular part of state, and only needs to re-render
when that part changes. For this we have the use_selector
hook.
#![allow(unused)] fn main() { extern crate yewdux; extern crate yew; use yewdux::prelude::*; use yew::prelude::*; #[derive(Default, Clone, PartialEq, Store)] struct User { first_name: String, last_name: String, } #[function_component] fn DisplayFirst() -> Html { // This will only re-render when the first name has changed. It will **not** re-render if any // other field has changed. // // Note: we are cloning a string. Probably insignificant for this example, however // sometimes it may be beneficial to wrap fields that are expensive to clone in an `Rc`. let first_name = use_selector(|state: &User| state.first_name.clone()); html! { <p>{ first_name }</p> } } }
Capturing your environment
For selectors that need to capture variables from their environment, be sure to provide them as
dependencies to use_selector_with_deps
. Otherwise your selector won't update correctly!
#![allow(unused)] fn main() { extern crate yewdux; extern crate yew; use std::collections::HashMap; use yewdux::prelude::*; use yew::prelude::*; #[derive(Default, Clone, PartialEq, Store)] struct Items { inner: HashMap<u32, String>, } #[derive(Clone, PartialEq, Properties)] struct DisplayItemProps { item_id: u32, } #[function_component] fn DisplayItem(props: &DisplayItemProps) -> Html { // For multiple dependencies, try using a tuple: (dep1, dep2, ..) let item = use_selector_with_deps( |state: &Items, item_id| state.inner.get(item_id).cloned(), props.item_id, ); // Only render the item if it exists. let item = match item.as_ref() { Some(item) => item, None => return Default::default(), }; html! { <p>{ item }</p> } } }
Listeners
Listeners are component-less subscribers. They are used to describe side-effects that should happen
whenever state changes. They live for application lifetime, and are created with init_listener
.
Here's a simple listener that logs the current state whenever it changes.
#![allow(unused)] fn main() { extern crate yew; extern crate yewdux; use yew::prelude::*; use std::rc::Rc; use yewdux::prelude::*; #[derive(Default, Clone, PartialEq, Debug, Store)] struct State { count: u32, } struct StateLogger; impl Listener for StateLogger { // Here's where we define which store we are listening to. type Store = State; // Here's where we decide what happens when `State` changes. fn on_change(&self, _cx: &yewdux::Context, state: Rc<Self::Store>) { yewdux::log::info!("state changed: {:?}", state); } } }
Can can start the listener by calling init_listener
somewhere in our code. A good place to put it is
the store constructor.
NOTE: Successive calls to init_listener
on the same type will do nothing.
#![allow(unused)] fn main() { extern crate yewdux; use std::rc::Rc; use yewdux::prelude::*; #[derive(Default, PartialEq, Debug)] struct State { count: u32, } struct StateLogger; impl Listener for StateLogger { // Here's where we say which store we want to subscribe to. type Store = State; fn on_change(&self, _cx: &yewdux::Context, state: Rc<Self::Store>) { yewdux::log::info!("state changed: {:?}", state); } } impl Store for State { fn new(cx: &yewdux::Context) -> Self { init_listener(|| StateLogger, cx); Default::default() } fn should_notify(&self, other: &Self) -> bool { self != other } } }
Tracking state
Sometimes it's useful to keep track of how a store has been changing over time. However this should
not be done in the listener itself. Notice Listener::on_change
takes an immutable reference. This
is necessary because otherwise we start to run into borrowing issues when listeners are triggered
recursively.
To track changes we can instead use a separate store that listens to the store we want to track.
#![allow(unused)] fn main() { extern crate yewdux; use std::rc::Rc; use yewdux::prelude::*; #[derive(Default, PartialEq, Debug)] struct State { count: u32, } #[derive(Default, PartialEq, Debug, Store, Clone)] struct ChangeTracker { count: u32, } struct ChangeTrackerListener; impl Listener for StateLogger { type Store = State; fn on_change(&self, cx: &yewdux::Context, state: Rc<Self::Store>) { let dispatch = Dispatch::<ChangeTracker>::new(cx); dipatch.reduce_mut(|state| state.count += 1); let count = dispatch.get().count; println!("State has changed {} times", count); } } impl Store for State { fn new(cx: &yewdux::Context) -> Self { init_listener(|| ChangeTrackerListener, cx); Default::default() } fn should_notify(&self, other: &Self) -> bool { self != other } } }
Contexts
Contexts contains the state of your Stores. You rarely (if ever) need to manage them manually, but it's useful to understand how they work.
You can easily create a new local context with Context::new
. Then just pass it into a dispatch and
you have your very own locally managed store!
#![allow(unused)] fn main() { extern crate yew; extern crate yewdux; use yew::prelude::*; use yewdux::prelude::*; #[derive(Clone, PartialEq, Default, Store)] struct Counter(u32); let cx = yewdux::Context::new(); let dispatch = Dispatch::<Counter>::new(&cx); }
Changes to one context are not reflected in any others:
#![allow(unused)] fn main() { extern crate yewdux; use yewdux::prelude::*; #[derive(Clone, PartialEq, Default, Store)] struct Counter(u32); let cx_1 = yewdux::Context::new(); let dispatch_1 = Dispatch::<Counter>::new(&cx_1); let cx_2 = yewdux::Context::new(); let dispatch_2 = Dispatch::<Counter>::new(&cx_2); dispatch_1.set(Counter(1)); dispatch_2.set(Counter(2)); assert!(dispatch_1.get() != dispatch_2.get()); }
The Global Context
You may already be familar with the global context. This is what you are using when you create a
dispatch with Dispatch::global
. The global context is thread-local, meaning you can access it from
anywhere in your code as long as it's on the same thread (for wasm this is effectively everywhere).
#![allow(unused)] fn main() { extern crate yewdux; use yewdux::prelude::*; #[derive(Clone, PartialEq, Default, Store)] struct Counter(u32); // These are equivalent! let dispatch_1 = Dispatch::<Counter>::global(); let dispatch_2 = Dispatch::<Counter>::new(&yewdux::Context::global()); dispatch_1.set(Counter(1)); assert!(dispatch_1.get() == dispatch_2.get()); }
IMPORTANT: Use of global context is only supported for wasm targets. See ssr support for more details.
SSR Support
By default Yewdux uses a global Context
that is shared thread-locally. This means we can share
state from anywhere in our code as long as it's within the same thread. Wasm applications are
strictly single threaded (without workers), so it isn't a problem.
However the same cannot be said for server side rendering. It is very possible the server is executing in a multi-threaded environment, which could cause various problems for Yewdux's single-threaded assumption.
While multi-threaded globally shared state is technically possible, it is currently not supported.
Instead Yewdux offers a custom component to hold your shared application state: YewduxRoot
. This
ensures all state is kept inside your Yew app.
#![allow(unused)] fn main() { extern crate yew; extern crate yewdux; use yew::prelude::*; use yewdux::prelude::*; #[derive(Default, Clone, PartialEq, Eq, Store)] struct State { count: u32, } #[function_component] fn Counter() -> Html { let (state, dispatch) = use_store::<State>(); let onclick = dispatch.reduce_mut_callback(|state| state.count += 1); html! { <> <p>{ state.count }</p> <button {onclick}>{"+1"}</button> </> } } #[function_component] fn App() -> Html { // YewduxRoot must be kept above all components that use any of your stores. html! { <YewduxRoot> <Counter /> </YewduxRoot> } } }
Yewdux hooks automatically detect when YewduxRoot is present, and use it accordingly.
SSR with struct components
For struct component support, refer to the higher order components pattern.
#![allow(unused)] fn main() { extern crate yew; extern crate yewdux; use std::rc::Rc; use yew::prelude::*; use yewdux::prelude::*; #[derive(Default, Clone, PartialEq, Eq, Store)] struct State { count: u32, } #[derive(Properties, Clone, PartialEq)] struct Props { dispatch: Dispatch<State>, } enum Msg { StateChanged(Rc<State>), } struct MyComponent { state: Rc<State>, dispatch: Dispatch<State>, } impl Component for MyComponent { type Properties = Props; type Message = Msg; fn create(ctx: &Context<Self>) -> Self { let callback = ctx.link().callback(Msg::StateChanged); let dispatch = ctx.props().dispatch.clone().subscribe_silent(callback); Self { state: dispatch.get(), dispatch, } } fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { Msg::StateChanged(state) => { self.state = state; true } } } fn view(&self, ctx: &Context<Self>) -> Html { let count = self.state.count; let onclick = self.dispatch.reduce_mut_callback(|s| s.count += 1); html! { <> <h1>{ count }</h1> <button onclick={onclick}>{"+1"}</button> </> } } } #[function_component] fn MyComponentHoc() -> Html { let dispatch = use_dispatch::<State>(); html! { <MyComponent {dispatch} /> } } #[function_component] fn App() -> Html { // YewduxRoot must be kept above all components that use any of your stores. html! { <YewduxRoot> <MyComponentHoc /> </YewduxRoot> } } }