Live data from Hacker News

How UI-driven state increases accidental complexity

evgenii.info

11–20 of 51 posts

Re: How UI-driven state increases accidental complexity

#11
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

Re: How UI-driven state increases accidental complexity

#12
post #9

Earlier quoted context omitted.

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…

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

Re: How UI-driven state increases accidental complexity

#13
post #2

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 bus…

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 fil…

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.

Re: How UI-driven state increases accidental complexity

#14
post #12

Earlier quoted context omitted.

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…

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

Re: How UI-driven state increases accidental complexity

#15
post #9

Earlier quoted context omitted.

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…

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.

Contexts were added to React to replace prop drilling.

Re: How UI-driven state increases accidental complexity

#17
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.

Re: How UI-driven state increases accidental complexity

#19
post #17

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…

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/

Re: How UI-driven state increases accidental complexity

#20
> 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.

Post reply on HN