
How UI-driven state increases accidental complexity - _elergy_
https://evgenii.info/ui-driven-state/
======
_bxg1
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.

~~~
viklove
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).

~~~
runawaybottle
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.

~~~
viklove
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.

~~~
runawaybottle
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.

~~~
viklove
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!).

~~~
nine_k
This sounds quite nice. I wonder if proper Redux stores could be used for that
(because you can create many of them).

------
jasim
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.

~~~
lkschubert8
I believe fable, f# transpiled via babel, would also fit into your list of elm
reason and pure script

------
dakom
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

------
btbuildem
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.

------
iaml
> 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.

------
supermatt
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.

------
wruza
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.

~~~
jamil7
Isn't this what selectors are for?

~~~
iaml
It is, gp is talking about the tech they don’t grok.

~~~
wruza
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](https://redux.js.org/recipes/computing-derived-data)

------
swalsh
That second "model" is insane... is that actually something people do?

~~~
gherkinnn
Oh yes.

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

------
bernawil

      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.

~~~
naasking
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.

------
PaulHoule
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.)

------
YuukiRey
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)

~~~
sbergot
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.

~~~
YuukiRey
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

------
goblin89
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.

------
catchmeifyoucan
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.

~~~
catchmeifyoucan
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.

------
Lapsa
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.

------
gregoronio
As I see it, the takeaway is that an accidental benefit of SQL is that it can
never resemble any UI?

------
_pmf_
This is not accidental complexity.

