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,
}

// The listener itself doesn't hold any state in this case, so we'll use an empty type. It's also
// possible to have stateful listeners.
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(&mut 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 replace the existing listener with the new one.


#![allow(unused)]
fn main() {
extern crate yewdux;
use std::rc::Rc;
use yewdux::prelude::*;
#[derive(Default, PartialEq, Debug)]
struct State {
    count: u32,
}
// Doesn't hold any state, so we'll use an empty type.
struct StateLogger;
impl Listener for StateLogger {
    // Here's where we say which store we want to subscribe to.
    type Store = State;

    fn on_change(&mut 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
    }
}
}

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>
    }
}
}