Yewdux State Management

repository·master·Indexed 43 days ago

https://github.com/intendednull/yewdux

Ergonomic state management for Yew applications version 0.13.0. It provides global state handling through hooks like use_store, a dispatch system, and the Store derive macro. Features include persistence via local or session storage, derived stores for synchronized state transformations, and HistoryStore for undo/redo functionality. It also includes utilities for handling HTML input events via the InputDispatch trait and FromInputElement.

Tokens
22.9K
Snippets
88
Records
98
Agent score
52%

What's inside Yewdux

  1. Overview of Yewdux for Yew state management

    master

    Yewdux is a state management solution designed for the Yew front-end library. It provides an ergonomic approach to global state management, allowing multiple components to access and manipulate shared state without the boilerplate typically required by Yew's built-in context manager.

    Key features include:

    • Simple: The core requirement is implementing the Store trait.
    • Ergonomic: Boilerplate is optional.
    • Predictable: Provides complete control over state changes.
    • Selective: Supports selectors to ensure components only re-render when specific parts of the state change.
    • Context agnostic: Dispatches can be created and executed from anywhere.
    • Complete component support: Works with both functional and struct components.
  2. How derived state works in Yewdux

    master

    Derived state creates a reactive link between a source store and a dependent store through the following lifecycle:

    1. Registration: A listener is registered to watch the source state.
    2. Trigger: When the source state changes, the dependency is notified.
    3. Update: The on_change implementation is executed:
      • DerivedFrom: Creates a brand new instance of the derived state.
      • DerivedFromMut: Modifies the existing derived state instance in place.
    4. Notification: Components using the derived state via hooks like use_store_value are notified and re-render.
  3. Create a local context with Context::new

    master

    You can create a private, locally managed store by instantiating a new yewdux::Context. This context is independent of the global state. To interact with this local store, create a Dispatch instance by passing a reference to the new context. Changes made to this dispatch will only affect this specific context and will not be reflected in the global context or any other local contexts.

    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);
  4. What are Listeners and how to use them

    master

    Listeners are component-less subscribers used to describe side-effects that should occur whenever a specific state changes. They live for the entire application lifetime.

    To create a listener, implement the Listener trait for a struct. You must specify the type Store to indicate which store the listener is subscribing to, and implement the on_change method to define the side-effect. Note that on_change receives an immutable Rc<Self::Store>, which prevents borrowing issues during recursive triggers.

    use std::rc::Rc;
    use yewdux::prelude::*;
    
    #[derive(Default, Clone, PartialEq, Debug, Store)]
    struct State {
        count: u32,
    }
    
    struct StateLogger;
    
    impl Listener for StateLogger {
        type Store = State;
    
        fn on_change(&self, _cx: &yewdux::Context, state: Rc<Self::Store>) {
            yewdux::log::info!("state changed: {:?}", state);
        }
    }
  5. Use the Global Context for state management

    master

    The global context allows you to access and manage state from anywhere in your code on the same thread. This is the standard way to use Yewdux for application-wide state. You can access it using Dispatch::<T>::global().

    Note: Global context is only supported for wasm targets. For Server-Side Rendering (SSR) support, refer to the SSR documentation.

    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));
  6. Define a state store with the Store derive macro

    master

    To create a manageable state in Yewdux, define a struct and derive Store, Default, Clone, and PartialEq. The Store derive macro enables the struct to be used with Yewdux hooks like use_store.

    #[derive(Default, Clone, PartialEq, Store)]
    struct State {
        count: u32,
    }
  7. Enable SSR support using YewduxRoot

    master

    By default, Yewdux uses a thread-local global Context. While this works for single-threaded Wasm applications, it is not safe for multi-threaded Server Side Rendering (SSR) environments.

    To support SSR, you must wrap your application (or the components using stores) in the YewduxRoot component. This ensures that all shared state is contained within the Yew application instance rather than relying on thread-local globals. Yewdux hooks automatically detect the presence of YewduxRoot and switch to using the context provided by it.

    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>
        }
    }
  8. Persist state using the #[store] macro

    master

    You can persist your Yewdux state in either local or session storage by using the #[store] macro on your state struct. Your struct must implement Serialize and Deserialize from serde to allow the state to be converted to and from a storage format.

    use yewdux::prelude::*;
    use serde::{Serialize, Deserialize};
    
    #[derive(Default, PartialEq, Serialize, Deserialize, Store)]
    #[store(storage = "local")] // can also be "session"
    struct State {
        count: u32,
    }
  9. Define a Store using the Store derive macro

    master

    A Store represents state shared application-wide. It is initialized on first access and lives for the application lifetime. The easiest way to define a store is by using the #[derive(Store)] macro on your state struct.

    Note: When using the macro, your struct must also implement Default and PartialEq.

    use yewdux::prelude::*;
    
    #[derive(Default, PartialEq, Store)]
    struct State {
        count: u32,
    }
  10. Initialize a Listener with init_listener

    master

    To activate a listener, call init_listener with a factory function that returns an instance of your listener and the current yewdux::Context.

    A recommended pattern is to call init_listener within the Store::new implementation of the store you wish to monitor.

    Note: Successive calls to init_listener for the same type will have no effect.

    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
        }
    }
  11. Implementing predictable mutations with Reducer

    master

    For more complex state logic, you can implement the Reducer<T> trait for a custom message type. This allows you to define explicit actions (messages) that describe how the state should change.

    Once implemented, you can use dispatch.apply(msg) to trigger the mutation or dispatch.apply_callback(f) to create a Yew Callback for event handlers.

    Tip: When implementing apply, you can use Rc::make_mut to perform Copy-on-Write (CoW) mutations on the Rc<State>.

    #[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();
    
    // Apply a message immediately
    dispatch.apply(Msg::AddOne);
    
    // Create a callback for an event handler
    let onclick = dispatch.apply_callback(|_| Msg::AddOne);
    
    html! {
        <button {onclick}>{"Increment (+1)"}</button>
    }