intendednull / yewdux

Ergonomic state management for Yew applications
https://intendednull.github.io/yewdux/
Apache License 2.0
319 stars 31 forks source link

Computed values in Yewdux Store #39

Closed emonidi closed 1 year ago

emonidi commented 1 year ago

Hi guys I am very new to Yewdux and Yew and I am struggling how to create a computed value in a store that derives from two other values in the store. This value has relatively many calculations and I would not like this code to be a part of a component file. I tried intercepting the state in a Listener but I can not seem to update the store from the listener on_change() function, which I realize is not a good idea sinse I could set an infinite loop in motion. Any ideas how can I achieve that without invoking the dispatch in the component code?

intendednull commented 1 year ago

Great question, this is actually a fairly common pattern. I would suggest modifying the derived value immediately, by embedding the behavior into your type. This also has the added benefit of making the behavior super transparent.

Here's an example of a couple ways to do that:

struct Foo {
    a: u32,
    b: u32,
    c: u32,
}

impl Foo {
    fn update_c(&mut self) {
        self.c = self.a + self.b;
    }

    fn set_a(&mut self, val: u32) {
        self.a = val;
        self.update_c();
    }

    fn set_b(&mut self, val: u32) {
        self.b = val;
        self.update_c();
    }

    fn with_update(&mut self, f: impl FnOnce(&mut Self)) {
        f(self);
        self.update_c();
    }
}

fn main() {
    let mut foo = Foo { a: 0, b: 0, c: 0 };

    // Setters for each relevant field. This is nice for simpler uses, but not very efficient 
    // because update is called more than necessary.
    foo.set_a(2);
    foo.set_b(1);

    assert_eq!(foo.c, 3);

    // Closure pattern is more efficient for complex modifications. Though it might be better 
    // to also use setters here, so you're not exposing the fields.
    foo.with_update(|state| {
        state.a = 5;
        state.b = 5;
    });

    assert_eq!(foo.c, 10);
}

To make sure update is always called, try defining the type in a dedicated mod, and keeping the fields private. That way you are forced to go through the provided methods, with no possible way to accidentally use the type wrong.

emonidi commented 1 year ago

Thanks a bunch for the advice. I ended up doing something similar but again I am bothered that I need to expose the method externally and call it from outside rather than define the calculation internally and rely that on every store change or better yet on every composing value change the derived value would be recalculated automatically and component would be notified.

intendednull commented 1 year ago

Another approach you could take is breaking out the derived value into a separate store. Then you could use a listener, as you initially intended

intendednull commented 1 year ago

@emonidi this should work

#[derive(Store, PartialEq, Default, Clone)]
struct Foo {
    a: u32,
    b: u32,
}

#[derive(Store, PartialEq, Default, Clone)]
struct Bar {
    c: u32,
}

struct FooListener;
impl Listener for FooListener {
    type Store = Foo;

    fn on_change(&mut self, foo: Rc<Self::Store>) {
        Dispatch::new().set(Bar {
            c: foo.a + foo.b,
        });
    }
}
emonidi commented 1 year ago

@emonidi this should work

#[derive(Store, PartialEq, Default, Clone)]
struct Foo {
    a: u32,
    b: u32,
}

#[derive(Store, PartialEq, Default, Clone)]
struct Bar {
    c: u32,
}

struct FooListener;
impl Listener for FooListener {
    type Store = Foo;

    fn on_change(&mut self, foo: Rc<Self::Store>) {
        Dispatch::new().set(Bar {
            c: foo.a + foo.b,
        });
    }
}

Thanks for your reply. I ended up doing something like that.

use gloo_file::Blob;
use yewdux::{store::{Reducer, Store}, prelude::{Dispatch, Listener, init_listener}, dispatch};

use super::clips::Clip;

#[derive(Clone,Debug,PartialEq)]
pub struct Scene{
    thumbs:Vec<Blob>
}

 #[derive(Clone, Debug,PartialEq)]
pub struct TimelineStore {
  pub clips: Vec<Clip>,
  pub scenes: Vec<Scene>
}

impl Store for TimelineStore {
    fn new() -> Self {
       init_listener(TimeLineListener);
       TimelineStore { clips: Vec::new(), scenes:Vec::new() }
    }

    fn should_notify(&self, old: &Self) -> bool {
        self != old
    }
}

pub enum TimeLineActions {
    CreateScene
}

impl Reducer<TimelineStore> for TimeLineActions{
    fn apply(&self, mut state: Rc<TimelineStore>) -> Rc<TimelineStore> {
        match self {
            TimeLineActions::CreateScene => {
                //change state here and return new state
                state.clone()
            },
            _=>{
                state.clone()
            }
        }
    }
}

struct TimeLineListener;
impl Listener for TimeLineListener {
    type Store = TimelineStore;

    fn on_change(&mut self, state: Rc<Self::Store>) {
        let dispatch = Dispatch::<TimelineStore>::new();
        let state = state.clone();
        if state.scenes.len() < state.clips.len(){
           dispatch::apply(TimeLineActions::CreateScene); 
        }
    }
}