[][src]Crate yew_state

This crate provides ergonomic access to shared state via component wrapper, with optional local/session persistence and custom scoping.

Initially this was a PR, but became big enough to warrant a standalone crate.

If you have suggestions please open an issue, or join in on the discussion.

Quickstart

To get started use the SharedStateComponent wrapper or StateView component.

SharedStateComponent

Give your component any properties that implement SharedState then wrap it with SharedStateComponent.

IMPORTANT: Changes must be handled in the component's change method.

use yew::prelude::*;
use yew_state::{SharedHandle, SharedStateComponent};
use yewtil::NeqAssign;

#[derive(Clone, Default)]
pub type AppState {
    pub count: usize,
}

pub struct Model {
    handle: SharedHandle<AppState>,
}

impl Component for Model {
    type Message = ();
    type Properties = SharedHandle<AppState>;

    fn create(handle: Self::Properties, _link: ComponentLink<Self>) -> Self {
        handle.reduce(|state| state.count = 1);  // Magically set count to one for example
        Model { handle }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        true
    }

    fn change(&mut self, handle: Self::Properties) -> ShouldRender {
        self.handle.neq_assign(handle)
    }

    fn view(&self) -> Html {
        let onclick = self.handle.reduce_callback(|state| state.count += 1);
        let count = self.handle.state().count;
        html! {
            <p>{count}</p>
            <button onclick=onclick>{"+1"}</button>
        }
    }
}

pub type App = SharedStateComponent<Model>;

StateView

For something simpler, StateView can handle shared state with less boilerplate.

Keep in mind you can't selectively re-render changes this way.

use yew::prelude::*;
use yew_state::{view_state, StateView, SharedHandle};

type CountHandle = SharedHandle<usize>;

fn view_counter() -> Html {
    html! {
        <>
            { view_display() }
            { view_input() }
        </>
    }
}

fn view_display() -> Html {
    let view = view_state(|handle: &CountHandle| {
        html! {
            <p>{handle.state()}</p>
        }
    });
    html! {
        <StateView<CountHandle> view=view />
    }
}

fn view_input() -> Html {
    let view = view_state(|handle: &CountHandle| {
        let onclick = handle.reduce_callback(|count| *count += 1);
        html! {
            <button onclick=onclick>{"+1"}</button>
        }
    });
    html! {
        <StateView<CountHandle> view=view />
    }
}

Handling State

State handles provide an interface to shared state. SharedHandle for basic access, while StorageHandle also does persistent local/session storage.

IMPORTANT: Changes to state do not take effect immediately! New state must be handled in the component's change method.

state provides current state.

let state: &T = self.handle.state();

reduce can be used from anywhere to modify shared state.

// SharedHandle<MyAppState>
self.handle.reduce(move |state| state.user = new_user);

reduce_callback allows modifying shared state from a callback.

// SharedHandle<usize>
let onclick = self.handle.reduce_callback(|state| *state += 1);
html! {
    <button onclick=onclick>{"+1"}</button>
}

reduce_callback_with provides the fired event as well.

let oninput = self
    .handle
    .reduce_callback_with(|state, i: InputData| state.user.name = i.value);

html! {
    <input type="text" placeholder="Enter your name" oninput=oninput />
}

Custom Properties

SharedState can be implemented for any properties.

TODO: This could be a macro.

#[derive(Clone, Properties)]
pub struct Props {
    #[prop_or_default]
    handle: SharedHandle<AppState>,
}

impl SharedState for Props {
    type Handle = SharedHandle<AppState>;

    fn handle(&mut self) -> &mut Self::Handle {
        &mut self.handle
    }
}

Persistence

To make state persistent use StorageHandle instead of SharedHandle. This requires state to also implement Serialize, Deserialize, and Storable.

TODO: This could be a macro.

use serde::{Serialize, Deserialize};
use yew_state::{Storable, Area};

#[derive(Clone, Default, Serialize, Deserialize)]
struct T;

impl Storable for T {
    fn area() -> Area {
         
        Area::Session // Defaults to Area::Local
    }
}

Scoping

Sometimes it's useful to only share state within a specific scope. This may be done by providing a custom scope to SharedStateComponent or StateView:

pub struct MyScope;
pub struct MyComponent = SharedStateComponent<MyModel, MyScope>;

Example

This example demonstrates how two counters with different scopes can increment shared state independently.

use yew::prelude::*;
use yew_state::{view_state, StateView, SharedHandle};

struct FooScope;
struct BarScope;

type CountHandle = SharedHandle<usize>;

fn view_input<SCOPE: 'static>() -> Html {
    let view = view_state(|handle: &CountHandle| {
        let onclick = handle.reduce_callback(|count| *count += 1);
        html! {
            <button onclick=onclick>{"+1"}</button>
        }
    });
    html! {
        <StateView<CountHandle, SCOPE> view=view />
    }
}
fn view_display<SCOPE: 'static>() -> Html {
    let view = view_state(|handle: &CountHandle| {
        html! {
            <p>{handle.state()}</p>
        }
    });
    html! {
        <StateView<CountHandle, SCOPE> view=view />
    }
}

pub struct App;
impl Component for App {
    type Message = ();
    type Properties = ();

    fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
        Self
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        true
    }

    fn change(&mut self, props: Self::Properties) -> ShouldRender {
        true
    }

    fn view(&self) -> Html {
        html! {
            <>
                <h1>{"FooScope"}</h1>
                { view_display::<FooScope>() }
                { view_input::<FooScope>() }
                <h1>{"BarScope"}</h1>
                { view_display::<BarScope>() }
                { view_input::<BarScope>() }
            </>
        }
    }
}

This crate provides ergonomic access to shared state via component wrapper, with optional local/session persistence and custom scoping.

Initially this was a PR, but became big enough to warrant a standalone crate.

If you have suggestions please open an issue, or join in on the discussion.

Quickstart

To get started use the SharedStateComponent wrapper or StateView component.

SharedStateComponent

Give your component any properties that implement SharedState then wrap it with SharedStateComponent.

IMPORTANT: Changes must be handled in the component's change method.

use yew::prelude::*;
use yew_state::{SharedHandle, SharedStateComponent};
use yewtil::NeqAssign;

#[derive(Clone, Default)]
pub type AppState {
    pub count: usize,
}

pub struct Model {
    handle: SharedHandle<AppState>,
}

impl Component for Model {
    type Message = ();
    type Properties = SharedHandle<AppState>;

    fn create(handle: Self::Properties, _link: ComponentLink<Self>) -> Self {
        handle.reduce(|state| state.count = 1);  // Magically set count to one for example
        Model { handle }
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        true
    }

    fn change(&mut self, handle: Self::Properties) -> ShouldRender {
        self.handle.neq_assign(handle)
    }

    fn view(&self) -> Html {
        let onclick = self.handle.reduce_callback(|state| state.count += 1);
        let count = self.handle.state().count;
        html! {
            <p>{count}</p>
            <button onclick=onclick>{"+1"}</button>
        }
    }
}

pub type App = SharedStateComponent<Model>;

StateView

For something simpler, StateView can handle shared state with less boilerplate.

Keep in mind you can't selectively re-render changes this way.

use yew::prelude::*;
use yew_state::{
    component::{view_state, StateView},
    SharedHandle,
};

type CountHandle = SharedHandle<usize>;

fn view_counter() -> Html {
    html! {
        <>
            { view_display() }
            { view_input() }
        </>
    }
}

fn view_display() -> Html {
    let view = view_state(|handle: &CountHandle| {
        html! {
            <p>{handle.state()}</p>
        }
    });
    html! {
        <StateView<CountHandle> view=view />
    }
}

fn view_input() -> Html {
    let view = view_state(|handle: &CountHandle| {
        let onclick = handle.reduce_callback(|count| *count += 1);
        html! {
            <button onclick=onclick>{"+1"}</button>
        }
    });
    html! {
        <StateView<CountHandle> view=view />
    }
}

Handling State

State handles provide an interface to shared state. SharedHandle for basic access, while StorageHandle also does persistent local/session storage.

IMPORTANT: Changes to state do not take effect immediately! New state must be handled in the component's change method.

state provides current state.

let state: &T = self.handle.state();

reduce can be used from anywhere to modify shared state.

// SharedHandle<MyAppState>
self.handle.reduce(move |state| state.user = new_user);

reduce_callback allows modifying shared state from a callback.

// SharedHandle<usize>
let onclick = self.handle.reduce_callback(|state| *state += 1);
html! {
    <button onclick=onclick>{"+1"}</button>
}

reduce_callback_with provides the fired event as well.

let oninput = self
    .handle
    .reduce_callback_with(|state, i: InputData| state.user.name = i.value);

html! {
    <input type="text" placeholder="Enter your name" oninput=oninput />
}

Custom Properties

SharedState can be implemented for any properties.

TODO: This could be a macro.

#[derive(Clone, Properties)]
pub struct Props {
    #[prop_or_default]
    handle: SharedHandle<AppState>,
}

impl SharedState for Props {
    type Handle = SharedHandle<AppState>;

    fn handle(&mut self) -> &mut Self::Handle {
        &mut self.handle
    }
}

Persistence

To make state persistent use StorageHandle instead of SharedHandle. This requires state to also implement Serialize, Deserialize, and Storable.

TODO: This could be a macro.

use serde::{Serialize, Deserialize};
use yew_state::{Storable, Area};

#[derive(Clone, Default, Serialize, Deserialize)]
struct T;

impl Storable for T {
    fn area() -> Area {
         
        Area::Session // Defaults to Area::Local
    }
}

Scoping

Sometimes it's useful to only share state within a specific scope. This may be done by providing a custom scope to SharedStateComponent or StateView:

pub struct MyScope;
pub struct MyComponent = SharedStateComponent<MyModel, MyScope>;

Example

This example demonstrates how two counters with different scopes can increment shared state independently.

use yew::prelude::*;
use yew_state::{
    component::{view_state, StateView},
    SharedHandle,
};

struct FooScope;
struct BarScope;

type CountHandle = SharedHandle<usize>;

fn view_input<SCOPE: 'static>() -> Html {
    let view = view_state(|handle: &CountHandle| {
        let onclick = handle.reduce_callback(|count| *count += 1);
        html! {
            <button onclick=onclick>{"+1"}</button>
        }
    });
    html! {
        <StateView<CountHandle, SCOPE> view=view />
    }
}
fn view_display<SCOPE: 'static>() -> Html {
    let view = view_state(|handle: &CountHandle| {
        html! {
            <p>{handle.state()}</p>
        }
    });
    html! {
        <StateView<CountHandle, SCOPE> view=view />
    }
}

pub struct App;
impl Component for App {
    type Message = ();
    type Properties = ();

    fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
        Self
    }

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        true
    }

    fn change(&mut self, props: Self::Properties) -> ShouldRender {
        true
    }

    fn view(&self) -> Html {
        html! {
            <>
                <h1>{"FooScope"}</h1>
                { view_display::<FooScope>() }
                { view_input::<FooScope>() }
                <h1>{"BarScope"}</h1>
                { view_display::<BarScope>() }
                { view_input::<BarScope>() }
            </>
        }
    }
}

Re-exports

pub use component::view_state;
pub use component::SharedStateComponent;
pub use component::StateView;
pub use handle::SharedHandle;
pub use handle::SharedState;
pub use handle::StorageHandle;

Modules

component
handle

Ergonomic interface with shared state.

Enums

Area

An area to keep the data in.

Traits

Storable

Allows state to be stored persistently in local or session storage.