1 unstable release

0.1.0 Jan 31, 2022

#13 in #shared-state

Download history 8/week @ 2023-11-20 4/week @ 2023-11-27 1/week @ 2023-12-11 6/week @ 2023-12-18 1/week @ 2024-01-08 5/week @ 2024-02-12 15/week @ 2024-02-19 33/week @ 2024-02-26 19/week @ 2024-03-04

72 downloads per month

MIT license

66KB
1.5K SLoC

Yewdux

Simple state management for Yew applications.

Install

Add Yewdux to your project's Cargo.toml:

[dependencies]
yewdux = "0.7"

Usage

Dispatch is the primary interface for Yewdux. It allows shared mutable access to global application state, held in specialized agent-like containers called Stores.

Writing to shared state

Creating a dispatch is simple, just give it a store type.

use yewdux::prelude::*;

#[derive(Clone, Default)]
struct MyState {
    count: usize,
}

let dispatch = Dispatch::<BasicStore<MyState>>::new();

Note we're using BasicStore above, which is one of the default store types provided by Yewdux. It is also possible to define your own store type.

Mutating state is done through reduce. Here we immediately send a message to increment count by 1.

dispatch.reduce(|state| state.count += 1);

We may also create callbacks that do the same. This button sends the message every time it is clicked.

let onclick = dispatch.reduce_callback(|state| state.count += 1);
html! {
    <button {onclick}>{"+1"}</button>
}

If the callback parameter is needed, it may be accessed using the *_with variant. The following creates a new callback, then immediately calls it, incrementing count by 5.

let cb = dispatch.reduce_callback_with(|state, incr: usize| state.count += incr);
cb.emit(5);

Reading shared state

To read state we need only a bridge to receive it. State is received once when a bridge is created, and every time state is changed afterwards.

use std::rc::Rc;

use yew::prelude::*;
use yewdux::prelude::*;

...

struct MyComponent {
    dispatch: Dispatch<BasicStore<MyState>>,
    state: Option<Rc<MyState>>,
}

enum Msg {
    State(Rc<MyState>),
}

impl Component for MyComponent {
    type Properties = (); 
    type Message = Msg;

    fn create(ctx: &Context<Self>) -> Self {
        // Create a bridge to receive new state. Changes are handled in `update`.
        let dispatch = Dispatch::bridge_state(ctx.link().callback(Msg::State));
        Self {
            dispatch,
            state: Default::default()
        }
    }

    fn update(&self, ctx: &Context<Self>, msg: Msg) -> bool {
        match msg {
            // Receive new state
            Msg::State(state) => {
                self.state = Some(state);
                true
            }
        }
    }

    ...
}

Less boilerplate please

Setting up a bridge for every component can be cumbersome. A solution is provided to handle this automatically: WithDispatch and DispatchProps.

Simply give your component DispatchProps properties and wrap it with the WithDispatch component wrapper.

IMPORTANT: WithDispatch and DispatchProps must be used together, or your app will panic.

struct MyComponent;
impl Component for MyComponent {
    type Properties = DispatchProps<BasicStore<MyState>>; 
    ...
}
html! {
    <WithDispatch<MyComponent> />
}

Now your component will automatically receive updates to state. Its properties also behave exactly like a regular Dispatch, with the notable addition of a single method for getting state.

fn view(&self, ctx: &Context<Self>) -> Html {
    let onclick = ctx.props().reduce_callback(|s| s.count + 1);
    let count = ctx.props().state().count;

    html! {
        <>
        <p>{"Count is "}{ count }</p>
        <button {onclick}>{"+1"}</button>
        </>
    }
}

Did you notice we don't have to deal with an Option this way? The component wrapper postpones rendering until it receives state for the first time, making it a little more ergonomic to use.

Hint: to save a little typing, use a type alias

type MyComponent = WithDispatch<MyComponentBase>;

struct MyComponentBase;
impl Component for MyComponentBase { .. }

html! {
    <MyComponent />
}

Need custom properties?

WithDispatch wrapper works with any component that has properties which implement WithDispatchProps. Simply implement it for your properties and you're good to go! This is likely to be a macro in the future.

#[derive(Properties, Clone, PartialEq)]
struct Props {
    dispatch: DispatchProps<BasicStore<MyState>>,
    ...
}

impl WithDispatchProps for Props {
    type Store = BasicStore<MyState>;

    fn dispatch(&self) -> &DispatchProps<Self::Store> {
        &self.dispatch
    }
}

Persistence

Yewdux supports state persistence so you don't lose it when your app reloads. This requires your state to also implement Serialize, Deserialize, and Persistent.

use serde::{Serialize, Deserialize};
use yewdux::prelude::*;

#[derive(Clone, Default, Serialize, Deserialize)]
struct MyState { ... };

impl Persistent for MyState {
    fn area() -> Area {
        Area::Session // Default is Area::Local
    }
}

struct MyComponent {
    dispatch: Dispatch<PersistentStore<State>>,
}

A persistent store checks for previously saved state on startup, using default if none is found. State is saved on every change.

Functional

Yewdux supports functional!

Add it to your project:

[dependencies]
yewdux-functional = { git = "https://github.com/intendednull/yewdux.git" }

And enjoy the terse goodness:

use yew::{prelude::*, functional::*};
use yewdux::prelude::*;
use yewdux_functional::*;


#[derive(function_component(MyComponent))]
fn my_component() -> Html {
    let store = use_store::<BasicStore<MyState>>();
    let onclick = store.dispatch().reduce_callback(|s| s.count += 1);
    let count = store.state().map(|s| s.count).unwrap_or_default();

    html! {
        <>
        <p>{"Count is "}{ count }</p>
        <button {onclick}>{"+1"}</button>
        </>
    }
}

Examples

Complete working examples can be found here.

To run an example you'll need to install trunk, then run (replacing [example] with your desired example name):

trunk serve examples/[example]/index.html --open

Dependencies

~13MB
~243K SLoC