-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Comments
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.
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. |
@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). |
@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? |
@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. |
@MirrorBytes Do you know if the issue is affected by the specified |
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 |
@kellytk Potentially, I haven't played with @hgzimmerman I don't believe it's a huge issue considering. Are you talking about a global routing schema? |
@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? |
In my use cases, I haven't had enough global state to warrant updating only fractions of the state at once. 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. |
@hgzimmerman When components receive updated state is it persisted to fields of the component's struct? |
Yes. match msg {
Msg::GlobalUIChanged(global_ui_state) => {
self.global_ui_state = global_ui_state;
true
}
... |
@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. |
Regarding designs of different update granularity I have the following options:
@hgzimmerman, @MirrorBytes, I know you prefer coarse over fine, but what's your opinion of the hybrid design? |
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 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 |
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). |
@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.
I suspect a solution exists and I'd appreciate insights from anyone toward that end. |
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. |
As discussed on Gitter, the solution recommended by @hgzimmerman is to return |
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:
Thank you all. |
Even though this is closed, an alternative to using agents would be to stick your global state in a 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 |
@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. |
I'm working on a novel smart pointer, trying to come up with a solution better than I think that there are four approaches to global state at the moment:
They are all bad in their own unique ways:
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 I think something similar to 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. |
I have a working demo (yew master only though) here for an ergonomic 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 |
@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 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 Don't think it would solve the |
Another option is maybe extract to a separate lib, like |
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 #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. |
Awesome! Can't wait to see it!
EDIT: non-worker agents were never serializing, the type system was enforcing types to be serializable though |
@mkawalec idk if you are aware but druid (GUI Framework) has a feature that goes in that direction, maybe its worth a look |
@jstarry is there an issue for that yet? |
Not yet! |
I agree with other folks sentiments about creating something 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 |
@kellpossible @mkawalec has fixed this in #1195 (I believe) |
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. |
Would it be possible to do something like https://github.com/torhovland/blazor-redux but for yew.rs in rust? |
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 :) |
yew-state provides simple CoW shared state management. Has other neat features like persistent storage, custom scoping, and a functional interface. |
This can't work because |
RecoilJS and Jotai might also be worth considering for inspiration: https://github.com/facebookexperimental/Recoil |
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. |
normally would expect closing an issue as ambiguous as this with a reason for the closure no? |
Description
I'm submitting a ...
Yew apps I write typically need global state. Some details of the pattern are:
create
orchange
, to their component struct.Can that be improved somehow? If Yew could be changed to improve how global state is facilitated, what would that look like?
The text was updated successfully, but these errors were encountered: