50 comments

[ 4.0 ms ] story [ 97.5 ms ] thread
The proposed approach is correct for the proposed use-case. The problem is, lots of UI state isn't handsome, core data like in this example. What about storing the width of a resizable panel? Or the state of a dropdown that can be open or closed? Do you create a dozen little flags in your store for these? Or do you put a flag on each domain object (todo list item, for example) that's not actually relevant to your business logic (you don't want dropdown state getting stored in your database)? Typically you'd make this a local flag internal to each component instance, but what happens when something else in your UI cares about it and has to respond to it? And any of these answers will bind it to your UI concerns, in all different ways. In fact, this state is directly UI-pertinent. It doesn't mean anything outside of the UI.

There are no catch-all answers to these questions. In every case, when you decide where to put your state, you're making compromises. Often, very uncomfortable ones. It is my opinion that the most intrinsically hard thing about building and maintaining UIs is managing state. Unlike most software where "state" is really just either data or an implementation detail along the way to producing output, UIs have real, irregular, messy state baked into their bones. State that can't be eliminated by refactoring, because it really means something.

The way I handle this, at least in React, is to create a context provider that handles my component's view state, then write my UI element as a consumer of that view state by wrapping it in an HOC. Then, if any other component wants to subscribe to the particular user interaction that drives the component I'm writing, they can simply tap into the existing context with an HOC that I've already written for them.

This way view state is kept outside of my global store, my UI component and its state controller is fully composable and extractable (meaning more reusable), and any other UI element within the same context can view and update my UI component as it pleases.

Another great thing about this is that you can scope your view state (or context) however you want. You can put it on the page level, the app level, or really wherever you want. You don't have to worry about cleaning up or resetting state because the context provider will be unmounted when the user navigates away (unlike a redux store).

Can you describe this implementation with just vanilla js? What’s the gist here, you’re using namespaced events? I’m just curious what this would look like outside of the world of React.
There are a lot of approaches you can take with vanilla js, but if your UI follows a component architecture that's modeled as a tree (App > Pages > Widgets), what I'm saying is that you take all of your interactive UI components and separate them into two nodes (a parent and a child). The parent P holds the state and provides callbacks to all descendants, one of which is the child C that you are writing. C is a pure component, meaning it holds no state and only invokes callbacks. You can put any number of "display" components in between P and C, but they should still be able to communicate.

Now, if any components you build in the future also need access to P's state, you can easily implement it as a new pure component that is a descendant of P. Since it has access to the callbacks, it will be able to update our original component C by invoking them.

With this setup, your app is basically separated into two "classes" of components, providers and consumers. Each provider holds the state and callbacks specific to itself only, which means you can easily pull out an individual provider and consumer to reuse in a new place (instead of being dependent on a global store).

The biggest benefit here is that future components can interact with P and by extension C without changing any code in P or C. Furthermore, if you want two parallel widgets with independent state, you can just create two instances of P as siblings, with C as descendants of both of them. If you wanted to do the same with a global store, you would most likely have to update your implementation to support parallel state tracking.

Another benefit is that you don't have to "clean up" the state when P is discarded. For example, if you are using a global store, and you navigate to a new page, you have to implement a callback to clear the widget state from the global store or risk loading stale state when the user navigates back. With this approach, since P has been removed from the tree, its state will be discarded along with it.

The only stuff I put in my global store is stuff that is actually relevant to my entire application. For example the user's username and preferences.

So my initial feeling is that it almost seems like you are reimplementing base React. Isn’t this effectively prop drilling, with a bunch of child components lifting state up to the global parent component (so that they all get updates?). Apologies if I’m simplifying it.
Well I'm using React so I can't be re-implementing it :)

I'm not advocating for any ground breaking paradigms here, I guess my only point is that the common advice to just throw everything into Redux (global store) is bad advice. You should only put global state into your global store. The common alternative is to silo state and functionality/UI into one component. I'd argue the third alternative, taking the best of both worlds, enables far more productivity. Basically creating mini-Redux stores that are only focused on one bit of user interaction. These are far more reusable, and since they're scoped locally they eliminate a lot of edge cases (and they're composable!).

This sounds quite nice. I wonder if proper Redux stores could be used for that (because you can create many of them).
(comment deleted)
Contexts were added to React to replace prop drilling.
Since he's using the context api https://reactjs.org/docs/context.html not exactly.

Imagine you have a React tree with a 100 node depth. You would then need to walk all the way up those 100 nodes, but using the context api you can put in context consumer at the branches of your tree where it makes sense, so instead of walking up 100 nodes you walk up 3 nodes to input into the global context.

Hope that is a reasonable high level explanation of a thing that can be complicated without experience of it.

(comment deleted)
Store it in your DB for that user.
Shouldn’t the panel’s width be stored on the Panel component, and all items in it should probably just be width 100%, or styled to fit the Panel via CSS. That’s the last bit of UI state that I’d ever think to move up to the global store.

The dropdown would act in the same way, no? It mostly sets some local state, and your can then derive the data you need from the store (let’s say the drop down was just an on/off filter). But yes, you are right, you would need to think about a way to abstract this out as a data flag and not a ui state flag.

Specifically for dropdowns, you usually want to keep their open / closed position in the UI state explicitly, because you want to close an open dropdown when the user clicks on another dropdown.

I frankly think that this complexity is not accidental. It is the real complexity of the UI, which is often not realized. All the little things that need thought and, at least once, an implementation.

The original drop down can close itself when it loses focus. No need go into shared state for this usecase.
Losing focus is itself a stateful change on a UI element caused by another UI element. It's just that the frontend platform like the browser or OS manages that shared state for you.
Yes, this is key. There are many cases like this (and CSS pseudo-classes, and the drag and drop API, etc.) where the browser platform gives you convenient ways of avoiding the problem. And certainly, take advantage of those where possible. But they don't eliminate the problem in the general sense.
> lots of UI state isn't handsome, core data

Very true. My interpretation of the article was that it refers to "core data only". If you re-read the article as "How storing core data in a structure that is coupled to the UI increases accidental complexity" then I think you'll find it more agreeable.

Personally I throw stuff like popup state into local component state and the global important stuff into redux. Perhaps the author implicitly assumes that everybody does this.

Right- like I said, local state is the natural solution in some cases. But it's not always so clean-cut. Maybe dropdowns were a bad example.

Here's one I've run into: say you have a list of things that can be edited and then when you click "Save" it saves them all to the server. Each of those would need to track a "saving" state to drive its loading spinner. That saving state is transient and UI-specific, and it is tied to each instance of a list item in some sense, but it can't just be local to the list-item component because it needs to be a) set as part of global app logic, and b) read by, for example, the "save" button so it knows when you're allowed to click it again.

There's an entire spectrum of these gray cases and there's no single answer for "how far up to hoist things". You're always making compromises. This is one reason I prefer MobX over Redux, because the latter basically locks you into "global state" and "local state" and doesn't allow for a lot of in-betweens.

https://github.com/janestreet/bonsai is making some interesting distinctions between "UI state" and "model state". They're kept separate and they're working on utilities to manage the use of composed components.
I have mixed feelings about this - partially because a so called “UI State” stores relations between types. Flat stores are a lot like simple tables. I’ve yet to be convinced one approach is superior to the other. I agree with container components and it’s the containers job to shape the data. In theory if we had M projects and N tasks - we’d have to perform M*N lookups to get project names for each task.

Most client applications don’t expect a million records, and in the grand scheme either approach might not matter; however, maintaining object relations gives us a clear overview of how data links together, making it easier to debug and demystify.

for the example provided by the author - one way without redesigning is perhaps adding a timestamp to the data and adjusting the container component to return sorted results by time added may yield the expected results.
Another angle to consider is that even within the context of UI state, the natural shape of the data may not match the renderer's state.

For example, if you're building a "pong widget" - the UI state is pretty straightforward across the board (paddle position, ball position, etc.) - but that's going to have an "impedance mismatch" when targeting DOM vs. GPU. The former may want things in a left-to-right tree structure[0], the latter may want paddle-meshes first regardless of their position on the screen[1].

Separation of concerns is a wonderful- even essential thing, but can be tricky for large real-world projects.

[0] that's not really a problem here - CSS grid with named areas could solve it in this specific use-case

[1] e.g. to avoid shader switching

It's a trade off. UI-agnostic state is a leaky abstraction. Data doesn't appear out of nowhere - You would have to battle proper data selection on the server regardless. Only this time - with reduced user interaction context.
Controller. You need a controller. Or a smart model, in this simple case.

That’s what react/redux has done to you, selling this ‘immutable functional’ flavoured thing. While it is immutable at the programming surface, it is actually a series of complex updates with little to no help from the ‘store’ for convenient access. In pure js, when you want project.tasks, you just:

  class Project {
    get tasks() {
      return db.tasks.filter(x => x.project_id == this.id && !x.deleted)
    }
  }

  class Task {
    get project() {
      return db.projects.find(x => x.id == this.project_id)
    }
    del() {this.deleted=true}
  }

  db.tasks = [].map(Task)
  db.projects = [].map(Project)

  ... in a view:
  h(button, {bind:[cr, 'add_task']}, '+')
  cr.project.tasks.map(task =>
    ...
    h(check, {bind:[task, 'done']})
    h(button, {bind:[task, 'del']}, 'x')

  ... in a controller:
  add_task(project) {
    db.tasks.push(new Task({project}))
  }
In “immutable functional state transformer based on async-dispatched store”, which these re-whatever buzzwords are, you cannot have neither smart data items, nor a good controller that could join unrelated objects together, nor recombinable data sources, nor nice testable api boundaries. And when someone whispers MVC, you think about web 1.0 patterns, with it’s “MC in MVC is backend roundtrip” meaning.

This particular example is a set of five dead-simple classes (DB, Project, Task, TaskListController and TaskListView), almost all orthogonal and useful/testable on their own. Easy for imperative thinking and managing^, cause UI is suddenly imperative. You constantly battle with immutability, lack of control, state copy-transfer and tons of boilerplate for a simple action, can’t you see? You’re doing a monad bind operation by hand, because there is no do-notation in your language, and where there is, it exists and is called “do” for a reason. How did they miss that completely? /rant

^ e.g. want to see deleted tasks? Write a getter in a controller or a db (depending on locality requirements) and use it in a view as if it was a pojo array. Reimplement on demand to leave your tests and logic intact.

You may want to have a look at how re-frame[0] (ClojureScript) does this or redux-toolkit[1]. The issues you mention are discussed there and solved well.

There is a controller in the sense you are describing, but the reads and writes are clearly decoupled as separate, functional steps, while the data is just a plain composition of maps and lists.

Putting these operations into classes is a regression from this point of view. You suddenly get these artificial objects (models) that get harder to extend, reuse and change, because you have relations between objects that you somehow need to model and synchronize as well.

Imagine if databases worked this way. Gladly they don't. They have a uniform, declarative language that you use to interact with them and the data is just data, no matter if it is a Person, Cat or w/e.

So in a sense these libraries are re-inventing databases to handle GUI state in the browser, which is a good thing.

[0] https://github.com/Day8/re-frame

[1] https://redux-toolkit.js.org/

Isn't this what selectors are for?
It is, gp is talking about the tech they don’t grok.
Maybe.

I don’t grok how one can write 100+ ceremony loc to filter a todo by a “.completed” flag and still think it is okay to burn a barrel of cash and half a day to implement that [1]. I also don’t grok how this approach can work any “faster“ or battery/memory-saving or be used sanely in apps slightly bigger than my generic todolist. It is both millions of dollars of bootstraps, boilerplates and learning curves for what supposed to be a simple logic.

Maybe I’m wrong, but it feels almost like a conspiracy to justify 6.5-figure contracts and nothing more. And the rest of developers just rationalizing around this phenomenon.

[1] https://redux.js.org/recipes/computing-derived-data

Immutable data makes updates hard and undo easy. It's especially painful in C++.

Mutable data makes mutation easy and undo hard.

I don't know if there's a good solution. The command pattern is based off mutable data, and works, but has lots of boilerplate and implementation (to mutate the document forwards and backwards).

As I see it, the takeaway is that an accidental benefit of SQL is that it can never resemble any UI?
> The most of Redux-applications look alike. They have a similar file structure and reasonable test coverage. They use the same middlewares and same libraries to force immutability. The developers, who work on them, use the same devtools.

This assumption is not really correct in my experience - most of the projects I worked on use redux, but some of them use ducks/feature driven approach, some have reducers separate from components. Some use sagas, some thunks and rare examples use hooks/no sidefx management at all. None use immutablejs and some use immer to discard pain of doing deep {...oldState, field: {...oldStateField, val: newVal}}. What author describes is good advice though, the whole idea is you only keep actually global state normalized and use selectors to construct the representation you need.

That second "model" is insane... is that actually something people do?
I was also surprised by this, never seen this before.
Oh yes.

Rushing to the first workable solution, no code review to speak of, deploying, open the next issue. Repeat.

Not really. That looks much more like what a selector would output.
I believe that both approaches make sense at appropriate app layers, but the key is the ability to handle data structure evolution.

Being able to change data schema with little overhead allows to operate on a less normalized structure and adjust it more frequently. Meanwhile, designing a highly normalized schema and implementing complicated queries takes time and still doesn’t make it entirely proof against the future.

Nitpick: UI-agnostic data schema example is what conventionally is referred to as normalized—closer to a theoretical canonical form, independent of query convenience at runtime. The other example, where tasks are child nodes of a project, would be the denormalized one.

I wish there was more experimentation in the front end landscape. Right now every major framework mixes state, logic and view. It doesn't matter if your state and logic is extracted into hooks, at the end of the day you're still mixing things by importing modules that directly add state and logic to components. I'd love to see a non-trivial app written __without using a single useXXX or class component__. The only thing your view layer is allowed to do is dispatch an action, or call an effect, which is interpreted outside that view layer. Of course this raises all sorts of questions, such as what about 100 instances of the same dropdown component which now all need to store their state somewhere. But I'd still like to see what working with that would feel like. I think The Elm Architecture would be pretty close to that (or exactly that, I don't know).

(I'm not saying the above would be better than what we have, just that it seems like it should be properly explored)

I mean... have you heard about redux? This is pretty much what you are describing. If you are looking for UI experimentation js framework are a good resource ;-)

I have used redux in a medium sized app.

The advantage is that there are lots of documentation. Developper can lean on those resources and the result is an architecture that is shared easily among multiple teams.

The disadvantage is that handling asynchronous effects is unclear. Also sometimes adding a flag to manage some subcomponent state require a lot of work. It also clutters your centralized state. It can also be a pain to manage forms this way. And by that I mean managing the state of a form that is in the process of being filled.

Currently my favorite way of designing js apps is a mixed approach. Put all your business code in model classes. Only centralize the state that is important to your application. Sprinkle local states when appropriate. Use hooks & context to inject your model classes in the right components.

Haha I should have mentioned this probably but yes I use redux a lot. The thing is that I suspect that the vast majority of people mix and match redux and local state. And what I have in mind is a strict, 100%, no exceptions separation. Just for experimentation purposes.

> The disadvantage is that handling asynchronous effects is unclear.

To be honest to me this is one of the areas where redux really shines. I've had great success with both redux-saga and redux-observable

This is not accidental complexity.
The store is just a global variable. That's all it is, dressed up pretty.

You still have to structure your data, and scope it properly. Some of the state will live in the store, and some in the components. It will be different from application to application.

The more obvious solution here would be to use a selector instead of using the state directly. Then your 'view' of the data is not tied to the underlying structure of the state.

Sure, you can denormalize too - but all the more reason to use selectors then to restructure your data.

Don't model data based on the component hierarchy, but model it in the best possible way. But the "best" approach depends - the same trade-offs as in database schema design applies here.

If most operations require data joins then pick a de-normalized structure. Then most data is often available with a single hash lookup. Though this makes updates difficult and mistakes can and often do lead to inconsistent data.

A normalized data-structure which use ids to refer to related elements makes it easy to add, update, and delete entities; but reads might require multiple joins which in turn makes the code complex.

We can choose between the two by listing out the possible operations and deciding which cases are more frequent. But this is often a moving target in a growing application.

The best approach I've so far found is to design data structures in a way that "invalid states are impossible". I will not link to Yaron Minsky and Richard Feldman's excellent talks on this here. This principle often results in elegant data structures that would've eluded me otherwise.

The second addition that is necessary is to use a statically typed language. Elm, Reason, and PureScript are the only choices in front-end at the moment because of soundness, sum/union types, and exhaustive pattern matching. A typed functional language makes refactoring an easy, mechanical, and reliable process which suddenly makes our code much more malleable and hospitable than before.

I believe fable, f# transpiled via babel, would also fit into your list of elm reason and pure script
It's an interesting tension.

One reality is that you can model the domain really well, keep it decoupled from the UI, and be able to maintain the app for the long term. Unfortunately if you have any neurotypicals involved they are going to feel mentally and maybe even physically uncomfortable from the discipline and won't rest until it is all tangled up again.

Another approach is to make the system very shallow, design the data structures to represent the UI you want directly, and hopefully gain enough in simplicity that you can deal with long-term evolution in creative ways such as end-to-end versioning.

The difficulty of reconciling the two has been one of the problems with UML, RDF, Graph Viz, Low Code, etc.

Some of the description of a system is unrelated to how it is visualized, yet, if you don't support manual layout and routing the visualizations you make will be meaningless. (In 2020 people are still too polite to tell the makers of hairball graphs that they should go back to the drawing board, but in 2005 there was a "emperor's new clothes" phenomenon where people didn't trust their instincts -- at least now they'll be honest with somebody else.)

  In contrast, it doesn't happen with a denormalized, UI-agnostic state:
Isn't it the other way around? the UI-agnostic state IS the normalized version; the UI-aware state ready to be consumed is the denormalized one.

The choice is pretty simple to me: keep the State normalized, de-normalize it in selectors for consumption in the UI. Those selectors will look pretty much the same as the reducers you'd be writing anyway.

Came just to post this. The state he's described is definitely normalized, and the UI-specific one is denormalized.

And I think your advice is exactly right: project your normalized state into a view that's useful for consumption by other components. Basically, the same pattern we've been applying to relational databases for decades.