I think the obsession the react space has with "state management" is a by-product of how incredibly convoluted reacts rendering is.
The other reason is a lack of exposure to how other technologies for GUIs have handled state for decades.
Maybe look outside the react bubble and see how many of these "issues" just disappear when you stop acting like react is some fundamental particle of the web.
Frankly I feel state management is a difficult task on desktop apps as well, to the point that tracking spaghetti-shaped causation and control flow is beyond my mental abilities. Qt itself as well as many apps are rife with redundantly calculating state or redrawing GUIs when changing the same value multiple times, or changing two values which both affect an outcome (my StateTransaction pattern mostly alleviates this issue with a set of dirty bitflags and recomputing all state dependent on those bits, though the reactivity system is currently hard-coded and statically dispatched, and generalizes poorly to open-ended state or managing the local state of many dialogs of the same type). And one of the craziest errors caused by witnessing malformed intermediate values is https://github.com/Dn-Programming-Core-Management/Dn-FamiTra..., where a sloppily-written "load document" function redrew the UI in the middle of mutating document state, causing the GUI to crash after observing a broken invariant.
It saddens me that so much of research in developing better state management techniques is in such a bloated and dependency-laden environment as JavaScript on the web. I like QML's reactivity system, but its evaluation engine is JS-based, dynamically-typed, and dynamically-scoped, and the UI engine itself is a buggy mess. And GTK4's list APIs promise to be better than the clusterfuck of Qt Widgets/Quick's QAbstractItem{Model/View} system (which abstracts poorly over list/column/tree collections, and widget-internal, cross-widget, and cross-application drag-and-drop), but I haven't tried that either.
While I don't deal with the ui too much (and hate the area / complexity), the best system I've seen so far is .net's WPF with MVVM pattern. You wire up the ui elements to models and things just work. Data dependencies are explicit, useless redraws are not happening if you schedule the changes on the right thread, the whole system can raise a coherent "binding error" rather than explode. It's definitely a solution I hate the least.
I haven't worked with Qt so I don't know how the API looks,
But on a complex app, to see "dirty bitflags", "intermediate values", "redrew the UI in the middle of mutating document state", I feel what you feel, and I want to share what helps me and my team overcome this kind of complexity.
This happened a few years ago to my team. We alleviated this with several solutions. The most effective solutions is to 1.) separate UI logic/compositive component from UI visual/appearance component, 2.) separate UI component from actors.
1.) Separating logic from visual helps developers define problems separately, especially those around the required states, step-functions, and lifecycle. We made this separation because of three things: 1.) We observed that our programmers are simply divided into these two categories, 2.) The dependencies of these two sets of components are simply different (e.g. logic --depends-on-> actors and ownership/lifetime management modules, while visuals depends on DOM, rendering, setting up callbacks), 3.) It is actually good to have a high cohesion between UI and logic modules. You may want to use the same UI for a slightly different logic, and vice versa.
2.) Separating UI component from actors helps a lot with decoupling state changes and re-rendering, and actually gives developer a chance to separate/abstract/generalize concerns when things have gotten too complex to be written in a UI component.
Actors is what I call any kinds of data that can act, have their own agency. It can be what you call a service, worker. It can have a Timer-based or queue-based internal lifecycle-management that does not require interaction from user (very useful for things like updating background data at interval)
UI component can both "own" or "borrow" actors. "owned" actors dies when its parent component is destroyed, while "borrowed" actors does not die when its borrowing component is destroyed.
We decouple actors from UI component and set up bridge between them. UI component can signal actors by function call, and actors can get back to the UI component by using either a return value or event/callback system.
And the last but not least, writing app in a smartly typed language combined with functional domain modelling helps a lot (see about it here https://www.youtube.com/watch?v=Up7LcbGZFuo).
Sorry, I don't understand what you're getting at. When timers or user actions are triggered, what code runs, where is the state it modifies located (next to the specific timer/action, within a module or dialog's object, or globally), how does the function determine which parts of the UI to reload from state, and when does it reload the UI? (Is this explained in the video or not? I haven't watched it yet.)
About the video, I should have made it clear that the video only talks about functional domain modelling.
Before we got into where state should be and when is UI reload/rerender triggered. I'm going to tell you a little imaginary problem.
Imagine you are building an application for downloading several huge files.
1.) This app must have a download page to trigger the downloading of various files as well as monitor their statuses.
2.) a system to manage multiple downloads. It can queue multiple downloads but only one should run at a time. But, regardless of the download page is open or not, the downloader should run its download.
3.) a persistent notification system for the app to notify the user (e.g. if a download succeeded or failed). This notification should be persisting, meaning that if the user does not dismiss a notification, it will stay there. The UI for the notification system looks like a smart phone's one.
4.) There are several other pages in the app.
Before we got into asking where state should be placed, we should examine what actors are there. There are the "downloader", the "download page", the "notification system", the "notification UI".
Because we have these several actors, we need to assume that all of these have several local states. All of these actors need to be in the component tree, but not necessarily bound to the rendering lifecycle.
These dependency arrows form the component tree automatically. Now, let's examine the solution:
- The state that tracks the notification should be in the notification system. The user-facing message in a notification item should be a copy that is put into the memory of the notification system.
- The state that tracks the queue of multiple downloads and the function that schedule the downloads should live in the downloader. These scheduler will have its own timer-like mechanism to notify itself that it needs to run other task if one is finished.
- The download page "listens" to events and "borrows" data from the "downloader". So if the downloader makes a change, the downloader page will change too.
- Last, the notification UI. The notification UI lives longer than the download page, because the user can switch between page but the notification UI stays on. The notification UI listens to the notification system for changes in its state and borrows its state.
- If you pay attention to the dependency arrow, notification UI and and download page both are the descendant of the notification system, but only notification UI react to "change-signal" from the notification system. Download page should not react to any signal from notification UI (e.g. no rerender).
- This is an indicator that the notification system must not be bound into the re-render lifecycle of the tree of components. Notification UI should explicitly subscribe to the notification system in order to allow other page to ignore the notification system. In JavaScript/TypeScript it is pretty easy to implement a callback-based event emitter that can be attached/detached.
- The notification system and the downloader is what I call an actor that can, but not always, be bound into the render lifecycle. It lives with the component that spawns and destroy it, but is detached from it.
Almost true. As someone who have been working on a complex desktop app for 4 years now.
I found that state management solutions are always incomplete for the problems we have in our team.
When you say "stop acting like react", it is actually true that some mechanism needs to escape from React's render loop while retaining its tree-like structure. New members to the team will have to be introduced to the idea that an actor (object that acts on data) does not need to be the same entity as the React component itself. We end up taking a lot of concepts from other domains such as game engine, Rust's ownership/borrowing
What we ultimately need are that's not fulfilled by many state management libraries out there:
- Instead of global vs local, we need management of scope, referability, and intuitive dependency injection.
- Instead of state, we need management of actors.
- Management of lifetime, ownership, and borrowing (concept taken from Rust)
- A differentiator between actor, function, and data (which entity should or should not do stuffs)
- Two-way communication channel
- A consistent code semantic that makes sense to describe it all because TypeScript/JavaScript does not cut it.
This doesn’t work, the most sound comparison web developers can usually do is to jquery (speaking out of previous thread experiences, lots of them). I also see all this movement as digging more steps into the same pit they’re in, in hope that it would make a ladder out somehow. For two decades of building various business UIs on various platforms I’ve never felt that it was hard or error-prone at the level which could bother anyone to fix once and for all with some funny legs above head technique. But now it suddenly (well, gradually) became a thing everyone’s suffocating without.
The other reason is a lack of exposure to how other technologies for GUIs have handled state for decades.
Maybe look outside the react bubble and see how many of these "issues" just disappear when you stop acting like react is some fundamental particle of the web.