intendednull/yewdux

use_selector is composable, but dispatch isn't

ivmarkov opened this issue · 9 comments

use_selector is a great way to create reusable components that operate only on a slice of the global store and are otherwise unaware of what the global store type actually is (by parameterizing the component with a generic parameter that represents the store and passing via props the selector function).

The idea is you create a reusable component, along with its store slice and reducers and then it can be plugged in a bigger app. As this one.

No such luck with the dispatcher in yewdux though. Dispatcher always takes the global Store object. In pure Redux with pure untyped JavaScript, the global nature of the dispatcher is not a problem because:
(a) Redux actions are untyped;
(b) More importantly, the reducers for each slice of the store are written in such a way, that an unknown action always "falls-through" and returns the particular slice unchanged;
(c) Finally, the root reducer is composed in a way where each slice reducer sees each action. (reference)

Given the typed nature of Rust though, we need to "select" or - rather - "project" the root dispatcher into a local one by using a user-supplied projection function. Say, use_dispatcher(|state: &mut S| &mut state.my_slice) which only operates on the particular slice that the component is interested in updating.

Oh and forgot to mention that all that stuff where actions (or messages in yewdux speak) are Reducers instead of simple data carriers (you seem to have the Reducer pattern reversed and applied on the action rather than on the store) is a bit incompatible with the notion of middleware, which might want to e.g. log the action itself, or extract the data from the action and send it over the wire. Not to mention that not going via the action (the Reducer) route and mutating the store via a closure further breaks it. But I guess this belongs rather to #40.

I have the feeling that any effort to "shortcut" the redux boilerplate breaks a pattern. :) Happy to point me at the right direction if I've missed something.

I think your observation boils down to: "when you have compound/sliced global Store, and you use use_selector, then Reducer messages need to carry the key so the reducer and store know what element to act on".

I think your observation boils down to: "when you have compound/sliced global Store, and you use use_selector, then Reducer messages need to carry the key so the reducer and store know what element to act on".

I wish it was that simple. Here's a task: create a component with yewdust which is aware of one slice (slice as in the most generic sense, I don't mean Rust slices here!) of the store (say - a User struct or similar), but is otherwise generic over the global store type.

I think we are pretty close. Did you see my Relation<T> Store gist?

let id = props.id;
let (account, state) = &*use_selector(move |store: &Relation<Account>| store.get(id));
let dispatch = Dispatch::<Relation<Account>>::new();
let save_clicked = {
    let account = account.clone();
    dispatch.apply_callback(move |_| Mutation::Save(account))   // ie. commit local copy to server and mark state as saving
}

I think we are pretty close. Did you see my Relation<T> Store gist?

let id = props.id;
let (account, state) = &*use_selector(move |store: &Relation<Account>| store.get(id));
                                                  // ^^^ The global store (`Relation`) is NOT generic in this code.
let dispatch = Dispatch::<Relation<Account>>::new();
//                       ^^^ You are dispatching to the global store directly. Your component is hard-wired and knows about the global store.
let save_clicked = {
    let account = account.clone();
    dispatch.apply_callback(move |_| Mutation::Save(account))   // ie. commit local copy to server and mark state as saving
}

Check the two comments I pasted in the above code. Did you read my last message? I bolded "generic".

@intendednull I would appreciate your comments on #40 and #41 as well, thanks! Background: I don't think we are having "a beginner in Rust and yewdux" problem here. I'm genuinely trying to understand how yewdux will evolve with regards to middleware and components that can be generified by the root store type. This is conceptual.

Maybe something like this would work? Forgive typos, on mobile, so code is untested

enum Action {
    SetName(String)
}

#[derive(Default, Store)]
struct Cat {
    name: String,
}

impl Reducer<Cat> for Action {
    fn apply(&mut self, state: Rc<Cat>) -> Rc<Cat> {
        match self {
            Action::SetName(name) => Cat { name: name.clone() }.into()
        }
    }
}

#[derive(Default, Store)]
struct Dog {
    name: String,
}

impl Reducer<Dog> for Action {
    fn apply(&mut self, state: Rc<Dog>) -> Rc<Dog> {
        match self {
            Action::SetName(name) => Dog { name: name.clone() }.into()
        }
    }
}


#[function_component]
fn SetName<T>() -> Html 
    where 
        T: Store,
        Action: Reducer<T>,
 {
    let set_name = Dispatch::<T>::new().apply_callback(|_| Action::SetName("foo".to_string()));
    ...
}

Maybe something like this would work? Forgive typos, on mobile, so code is untested

Thanks. What is interesting in your suggestion is to use a separate store for each component, instead of trying to slice a single global store and deal with the complexities of fixing the dispatch function. In this case, generifying the store is pointless of course.

Yeah, breaking your global state into chunks is a great way to manage this problem.

Dispatching over a "slice" is an interesting idea, however I'm unsure if it can be expressed ergonomically enough to be practical. Might be one of those problems that Rust forces us to think about differently

Closing as the notion of slicing the store is not so urgent given that yewdux supports multiple stores.