Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global state #576

Closed
kellytk opened this issue Aug 11, 2019 · 40 comments
Closed

Global state #576

kellytk opened this issue Aug 11, 2019 · 40 comments
Labels

Comments

@kellytk
Copy link
Contributor

kellytk commented Aug 11, 2019

Description

I'm submitting a ...

  • question

Yew apps I write typically need global state. Some details of the pattern are:

  • Components persist properties they're passed, in create or change, to their component struct.
  • Properties have a global state struct field.
  • A shared properties struct exists for passing properties with only a global state field to a subcomponent.
  • Custom component properties structs are made for passing properties with a global state field, as well as others specific to the component, to a subcomponent.

Can that be improved somehow? If Yew could be changed to improve how global state is facilitated, what would that look like?

@hgzimmerman
Copy link
Member

While I don't think it is necessarily better in all situations, its possible to use the agents system to store global state instead of passing a global state prop to each component.

  • Component connects to the agent in create().
  • Agent adds component to a list of subscribers.
  • Component requests global state from agent still in create().
  • Component gets message from agent, handles it in the update() method.
  • Component wants to update global state. Sends message off with change to the agent.
  • Agent propagates change to all subscribers, syncing global state with all other components. (Note: I don't immediately recall if you can prevent the sender from also being updated. Ideally you would like to be able to choose if you update all components or just the ones that didn't send the mutation message to the agent).

You could get clever with this pattern and create a network of agents, consisting of senders and receivers, where components with receivers are subscribed to changes, while senders are able send messages without their dependent components being responsible for handling updates to changes in the global state.

@MirrorBytes
Copy link

@hgzimmerman That's exactly how I've rigged my applications with global state. One note however, make sure to bridge the state in the root component (even if the state is not being used there), otherwise its direct child components will fail to recognize changes by their sibling components (essentially, it'll spin up individual states for each child component of the root, if not bridged in root).

@kellytk
Copy link
Contributor Author

kellytk commented Aug 16, 2019

@hgzimmerman Should components ever unsubscribe from the global state agent?

@MirrorBytes I intend on trying @hgzimmerman's suggestion. Would you expand on what must be done to avoid the issue you warn of?

@MirrorBytes
Copy link

@kellytk You see how in the routing example that the Router agent is bridged in the root component? Make sure to bridge the State agent is bridged the same way, even if it's not being using in the root component. It needs to be instantiated as far up the component hierarchy as possible.

@kellytk
Copy link
Contributor Author

kellytk commented Aug 20, 2019

@MirrorBytes Do you know if the issue is affected by the specified Reach value? Do you think it should be considered a bug or is it intentional by design? Perhaps @deniskolodin could provide insight?

@hgzimmerman
Copy link
Member

I personally haven't ran into the requirement for the agent bridge to be constructed in the root component, although I don't think I've tried doing otherwise. If a minimal example project could be provided that exhibits this behavior, we could work towards implementing a fix. I'd look into creating that example myself, but I'm frantically trying to get a router component finished so I can take advantage of jstarrys nesting component changes and get feedback to him, as well as life priorities. Its on my list of things to do, but I'm not getting to it soon.

About components unsubscribing from agents: Yes, you should try to disconnect from agents when your component is destroyed. This is best handled in the disconnected() method in Agent, which will get a HandlerId which you can use to remove from your set of subscribers.

@MirrorBytes
Copy link

@kellytk Potentially, I haven't played with Reach to change its outcome. I believe this should remain in place considering it makes sense to be forced to instantiate in root (relatively speaking).

@hgzimmerman I don't believe it's a huge issue considering. Are you talking about a global routing schema?

@kellytk
Copy link
Contributor Author

kellytk commented Aug 24, 2019

  • Agent propagates change to all subscribers

@hgzimmerman, @MirrorBytes Do you propagate global state change via a single notification message and global state struct, multiple distinct notification messages and state values, or another design?

@hgzimmerman
Copy link
Member

In my use cases, I haven't had enough global state to warrant updating only fractions of the state at once.
So, without prior experience, I would say that you should have as many global state agents as you have "global features".

Say you have a key that you use when looking up localization for user-presented text, as well as a light/dark theme and font-size configuration. I would keep localization related things in one agent and UI (theme and font size) in another, and update all the related state for an agent in one message. Some components will care about localization, some will care about theming, some both. I think allowing components to pick and choose which agents to subscribe to, while not having super-granular control of each individual setting, strikes a nice balance between maintainability and performance.

@kellytk
Copy link
Contributor Author

kellytk commented Aug 24, 2019

@hgzimmerman When components receive updated state is it persisted to fields of the component's struct?

@hgzimmerman
Copy link
Member

Yes.
I've taken to storing the struct passed in the message directly in the component instead of dealing with individual fields.
So in update():

match msg {
    Msg::GlobalUIChanged(global_ui_state) => {
        self.global_ui_state = global_ui_state;
        true
    }
    ...

@MirrorBytes
Copy link

@kellytk Whenever you're sharing application state, it's generally a good idea to send the entire state as opposed to splitting it up UNLESS your app state is massive in terms of fields. So long as the state is stored within a components struct when passed to it (or an individual field of the app state), it will persist. Think of the fetch_task example for instance; if it isn't stored in the component struct, it won't process/persist.

@kellytk
Copy link
Contributor Author

kellytk commented Aug 28, 2019

Regarding designs of different update granularity I have the following options:

  • With coarse updates, subscribed components receive a single monolithic state value. That's simple, however the downside is components receiving update messages for elements of state that are irrelevant to them.

  • With fine updates, components subscribe to specific elements of state that are relevant to them and only receive update messages when they change. The downside is not only is there greater complexity, increased efficiency is debatable because what's saved by only receiving messages pertaining to relevant elements of state may be offset by receiving more messages.

  • I believe a hybrid solution where components can either subscribe to a single monolithic state or distinct state elements would be the most complex but also the most efficient.

@hgzimmerman, @MirrorBytes, I know you prefer coarse over fine, but what's your opinion of the hybrid design?

@hgzimmerman
Copy link
Member

hgzimmerman commented Aug 28, 2019

In general, while the mechanism agents use to move data around is slow (serialize data, copy string, deserialize data), the total volume of data being moved around is usually tiny (at most 100 components on screen at a time, and rarely more than 1kib per transaction if your state doesn't use strings heavily). Unless you are handling high numbers of components (eg. a dynamically themeable css framework component library), the most likely situation is 10ish components synchronizing 100 bytes worth of state (strings notwithstanding).

In situations like this, I would hazard a guess that this amount of data being synchronized wouldn't cause you to "drop a frame" (16ms) or cause perceivable delay (50-200ms) to the user. So, in all, unless you notice these things, then performance shouldn't be a concern. And even if these problems do present themselves, a lot of yew's slowness comes from interoperation between WASM<->JS<->Browser, and reduced payload size for global state synchronization will still likely be dwarfed by this.

So I would always prefer the simpler and less complex solution over one that is error prone. For me that means a coarse updating model, and the first optimization step would be to partition the Agent into many Agents along functional boundries, instead of adding more fine-graned message variants to a single Agent and keeping different subscriber lists within it.

ALL that said, I think that it is preferable to have agents accept messages that only update part of the state, but always broadcast their entire state to their subscribers when anything changes.


It would still be interesting to test the limits of this. As a project idea:

Have a component with a text box. Every character you type, it sends the text-box's value to an agent.

The agent holds state of color: String and data: Vec<u8>. When it updates the color, broadcast its new state to all subscribers.

Another component type can also be subscribed to the agent. Its just a fixed-size div that sets its color based on state received from the agent.

Create 100 of these components, and initialize data to 100 elements long. See if there is any delay for updating these divs when setting the color. Find the breaking point - how many components / how much excess data does it require for a noticeable slowdown. Does shrinking the data size do more to improve performance than shrinking the number of components?

@tracker1
Copy link

Maybe a bit offtopic, but I can't help but think it might be worthwhile to look at the process React and Redux use together to handle these type of things... My first thought is to use a store similar to how Redux works, and building a subscriber model with maps to the portions of state a given component needs and subscribes to. As well as tiering from a primary reducer for handling actions (dispatched via event subscribers).

@kellytk
Copy link
Contributor Author

kellytk commented Oct 7, 2019

@hgzimmerman The design you describe is similar to a pattern I've used for abstracting agents which manage a WebSocket connection and implement a network protocol. I've nearly completed reimplemention of global state with the pattern and the initial results are promising.

A disadvantage I've observed with agent-based global state management is that there appears to be an easy and convenient path toward inefficiency. When several components in a hierarchy subscribe to state and receive change notifications, a component will be rerendered for each ancestor component subscribing to the state and rerendering + 1.

- ComponentA1 (subscribes to state, rerendered once upon change)
  |- ComponentA2 (subscribes to state, rerendered twice upon change)
     |- ComponentA3 (subscribes to state, rerendered thrice upon change)
- ComponentB1

I suspect a solution exists and I'd appreciate insights from anyone toward that end.

@hgzimmerman
Copy link
Member

hgzimmerman commented Oct 8, 2019

If each component compares its persisted props against its new props passed down from above, then it should ignore the requests to rerender due to prop changes, because nothing should be changing after a global state change.

You should be stuck with a O:n number of prop comparisons and rerenders (although the number of prop comparisons is likely to be higher if you have a per-component component branching factor greater than 1, which is pretty much guaranteed).

Unless I'm misunderstanding something.

@kellytk
Copy link
Contributor Author

kellytk commented Oct 9, 2019

As discussed on Gitter, the solution recommended by @hgzimmerman is to return false from change when Self::Properties is ().

@kellytk
Copy link
Contributor Author

kellytk commented Oct 9, 2019

I've successfully concluded the experiment of managing global state via an agent.

A couple of my preferred designs' implementation details that may be of use to others:

  • Agent Request::RegisterNotifications(Vec<Notification>) allows a component to conveniently subscribe to multiple Notifications.
  • One agent Response for each element of state that may be subscribed to changes of.
  • Agent struct fields ala notification_observers: Vec<HandlerId> allow multiple components to subscribe to a specific Notification.

Thank you all.

@kellytk kellytk closed this as completed Oct 9, 2019
@hgzimmerman
Copy link
Member

Even though this is closed, an alternative to using agents would be to stick your global state in a Rc pointer, and pass that down through props. This way, its super cheap to propagate (only a ptr clone and a ref-count increment). You can pass a callback to the root for updating this global state to components that are responsible for updating it.

While the Agent-based approach is acceptable, I think this will be my approach going forward (at least for settings-like global state), as the performance and simplicity gains outweigh the annoyance of including a Rc<GlobalState> field in most of my Props.

@kellytk
Copy link
Contributor Author

kellytk commented Nov 8, 2019

@hgzimmerman The agent-based design does indeed incur additional overhead. While I'm content with my implementation for the time being, I'm also not convinced that it's the optimal solution.

I had briefly implemented the design you describe in my transition from a flat properties-based solution to the agent-based solution. Having used the three designs I think it's a promising avenue for experimentation.

@kellytk kellytk reopened this Nov 8, 2019
@hgzimmerman
Copy link
Member

I'm working on a novel smart pointer, trying to come up with a solution better than Rc for Yew. Its not ready to be used, and doesn't offer anything over Rc except in rare situations, but it has gotten me to think about how smart pointers work in Yew over the past few days, and some of that thought crosses into concerns about global state.

I think that there are four approaches to global state at the moment:

  1. Coordinate state via agents.
  2. Use lazy_static/once_cell + RefCell to get a global mutable reference.
  3. Pass a clonable global state struct in Props.
  4. Pass smart pointers to global state in props.

They are all bad in their own unique ways:

  1. Agents are "slow", require dispatchers/bridges (or links) to be attached to component models.
  2. Real globals can be used anywhere, making it hard to audit their use.
  3. Copying state through multiple components can be expensive, and they take up space in Props.
  4. Smart pointers need to be passed in Props.

And all four lack a way of dictating where the data can be mutated. I want to be able to have global state and be able to dictate that "only these components can mutate global state, the others can only read". Similar to & and &mut, but without the restriction that there can only be one "mutable reference", and with ownership over the data so it can work within Yew's constraints for Prop lifetimes.

I think something similar to Rc could fit the bill, but without a Weak variant and its associated additional reference counter, and with the ability to create Immutable Rcs, so that you can pass these around to components and be confident that nothing has modified them along the way when it comes time to make display decisions regarding them. There isn't a reason to have Weak ptrs because Yew's tree structure should prevent you from creating Rc cycles, making that concern irrelevant.
You should be able to get this behavior by newtyping Rc and disallowing mutation in one of the wrappers, but reimplementing from scratch would allow you to eliminate the now useless weak_count field in Rc for extremely minor space/performance savings.

You would still have the annoyance of these being present in your properties, but it should be obvious if its a handle that can be mutated, or if its just a "view" into the "global" state.

@hgzimmerman
Copy link
Member

I have a working demo (yew master only though) here for an ergonomic Rc-alike pointer: https://github.com/hgzimmerman/yewtil/blob/master/src/ptr/mrc.rs
and an example here: https://github.com/hgzimmerman/yewtil/blob/master/examples/mrc_irc/src/main.rs

I'll "dogfood" this myself over the coming months and report back how it works, but I think its a nice little ergonomic upgrade over normal Rcs for application use.

@DukeFerdinand
Copy link

@hgzimmerman New to Rust so sorry if this is a dumb question, but is there any way to gain the best of both worlds with agent-based state and Rc based references to it? I ran into the .clone() pattern of returning refs to the global state across agents, but the alternative of passing state down everywhere is not one that's sustainable. It gets very confusing very quickly haha

I've got a demo project that I'm cleaning up with agents that pass enum variant "mutation" and "getter" actions, so maybe once I can link that it'll make more sense if an Rc<Store/State> can be put in there somehow.

Don't think it would solve the .clone pattern quite yet, but it at least makes it where components selectively "subscribe" or don't and can still receive props passed down (a lot like how Redux and Vuex handle things in JavaScript). One bad thing is the component-level reducers adds some overhead to declaration, but that might be solved when I can iterate on that design with some extraction into a trait or something.

@DukeFerdinand
Copy link

Another option is maybe extract to a separate lib, like yewx as a play on Vuex from vue

@mkawalec
Copy link
Contributor

mkawalec commented May 4, 2020

Something I would personally prefer would be a Redux model, with subscriptions to individual properties and a global state that components can lens into depending on their individual needs. Actors could be a crude approximation, but they are very manual and crucially do not allow for passing JsValues between components and actors because of the serialization step.

#1026 is a great effort, but it's still missing the global state part, I think I will take a stab at creating such a creature.

@jstarry
Copy link
Member

jstarry commented May 4, 2020

Awesome! Can't wait to see it!

Btw I think we should definitely make the non-worker agents skip serialization


EDIT: non-worker agents were never serializing, the type system was enforcing types to be serializable though

@pythoneer
Copy link

@mkawalec idk if you are aware but druid (GUI Framework) has a feature that goes in that direction, maybe its worth a look
https://xi-editor.io/druid/lens.html
https://docs.rs/druid/0.5.0/druid/trait.Lens.html
https://docs.rs/druid/0.5.0/druid/struct.LensWrap.html

@kellpossible
Copy link
Contributor

Btw I think we should definitely make the non-worker agents skip serialization

@jstarry is there an issue for that yet?

@jstarry
Copy link
Member

jstarry commented May 6, 2020

Not yet!

@haywoood
Copy link

I agree with other folks sentiments about creating something react-redux ish to connect components to state, but wanted to say we shouldn't be trying to copy the APIs (#1026) that are used in JS land if we have language features that enable us to do better.

Clojurescript is a great example of achieving what's enabled by React+Redux but with a way simpler API. You store data in these wrappers, and inside a component's render function, when you dereference the wrapper, it gets added to the list of inputs which cause the component to update when the value changes.

More info on that here: https://github.com/reagent-project/reagent/blob/master/doc/ManagingState.md

But it's that mechanism that allows for a robust subscription / event dispatching system to manage a global (single source of truth) through the re-frame library.

https://github.com/day8/re-frame/blob/master/docs/ApplicationState.md
https://github.com/day8/re-frame/blob/master/docs/CodeWalkthrough.md

@teymour-aldridge
Copy link
Contributor

teymour-aldridge commented Jun 11, 2020

Btw I think we should definitely make the non-worker agents skip serialization

@jstarry is there an issue for that yet?

@kellpossible @mkawalec has fixed this in #1195 (I believe)

@evelant
Copy link

evelant commented Aug 5, 2020

I'm very new to rust, just starting to learn and investigate yew but I have plenty of react/TS experience and (IMO) the best thing to ever happen in the JS/TS world for state management is mobx-state-tree. It's worth a look at how mobx and mobx-state-tree work. https://mobx-state-tree.js.org/intro/philosophy https://mobx.js.org/README.html

IMO mobx-state-tree is light years ahead of the redux pattern. Like not even close. There are so many difficult problems in redux (and loads of boilerplate) that just don't exist with the transparent reactivity of mobx combined with the structured data and actions of mobx-state-tree. Fine grained reactivity means perfect render performance (never renders when not necessary) with zero boilerplate. Transparent reactivity means zero boilerplate. Batched mutations via actions means clean one way data flow, no transient bad states, and easy async actions. Tree model with actions and patches gives immutable-like time travel, replay, serialization, and validation with no overhead or boilerplate. It seems like magic when you first use it but it works flawlessly.

It looks like somebody is already working on a mobx pattern implementation in Rust https://github.com/s-panferov/observe

Again I'm really too new to Rust to know fully how well different state management patterns might work, but seeing the talk of redux here I figured it would be useful to bring mobx into the discussion as a pattern to investigate.

@lukechu10
Copy link
Contributor

Would it be possible to do something like https://github.com/torhovland/blazor-redux but for yew.rs in rust?

@mkawalec
Copy link
Contributor

Generally it is possible to implement any data sharing pattern in Rust. In some cases it can be done even more effectively than in JS, as we can truly share data immutably with no runtime cost. It would be awesome to see more data management innovation in web Rust, so feel free to implement it :)

@intendednull
Copy link
Contributor

yew-state provides simple CoW shared state management. Has other neat features like persistent storage, custom scoping, and a functional interface.

@lukechu10
Copy link
Contributor

2. Use lazy_static/once_cell + RefCell to get a global mutable reference.

This can't work because RefCell isn't Send + Sync.

@Oyelowo
Copy link

Oyelowo commented Jan 2, 2021

RecoilJS and Jotai might also be worth considering for inspiration:

https://github.com/facebookexperimental/Recoil
https://github.com/pmndrs/jotai

@kellytk kellytk closed this as completed Apr 28, 2021
@lukechu10
Copy link
Contributor

Hi there. Not sure why you closed this. As far as I am aware, there still isn't an (official) ergonomic pattern to achieve this in Yew.

@kellpossible
Copy link
Contributor

normally would expect closing an issue as ambiguous as this with a reason for the closure no?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests