DEV Community

Magne
Magne

Posted on • Updated on

What happened to Components being just a visual thing?

I really like when Components are actually just visual components.

Like the JSX specification indicated:

// Using JSX to express UI components.
var dropdown =
  <Dropdown>
    A dropdown list
    <Menu>
      <MenuItem>Do Something</MenuItem>
      <MenuItem>Do Something Fun!</MenuItem>
      <MenuItem>Do Something Else</MenuItem>
    </Menu>
  </Dropdown>;

render(dropdown);
Enter fullscreen mode Exit fullscreen mode

It makes me able to look at a page in the browser, and go into the code and intuitively just pattern-match what I saw in the browser to the structure in the markup and immediately see where I have to make the change. But now.. I find myself increasingly having to actually cognitively 'parse' the markup/JSX before I can find my point of inflection. (Don't Make Me Think applies for developers just as well as end-users. That's why technologies that feel familiar, like JSX, have generally won.)

I feel that putting all sorts of logical components into the rendering is abusing the original idea of components.

The original idea was:

"React Component: A highly cohesive building block for UIs loosely coupled with other components." as Pete Hunt defined it when introducing React in 2013

I like to think of Components as encapsulating UI structure and behavior, and styling (ideally, too), based on a single unifying concern.

And a Component was supposed to be denoted with a JSX tag, similar to HTML elements (like in the above example from the JSX specification).

But I more and more frequently see the abuse of components (and/or their syntax). Do any of the following examples correspond to the original definition of Components mentioned above?

  • <Provider> and <Query> components in Apollo Client.
  • <Router> and <Route> components in React Router.
  • <For> components in SolidJS, react-loops, react-for and react-listable, jsx-control-statements, and react-condition...
  • <Show> components in SolidJS. Basically a (reactive) reimplementation of an if-statement.
  • <Suspense> components in SolidJS and ReactJS. Also basically a reimplementation of an if-statement, albeit an async one.
  • <ErrorBoundary> components in React and SolidJS.
  • Multiple versions of essentially the same visual component, like: <ChartContainer> and <UnconditionalChartContainer>. Leaking implementational details into the rendering markup.

It makes me sad.

  • And now, with three new component types to consider: 'Server Components', 'Client Components' and 'Shared Components', when looking at a component, I will even have to know where it is rendered (since it just might deviate from the convention). (Not to mention knowing if it was serialised properly, or if I was stupid enough to pass an anon function into it, mistakenly thinking it was a client component..). "Was it Client Components that couldn't be passed into Server Components, or was it the other way around, now again?" But at least client and server components have something to do with rendering, and are not simply logic-posing-as-markup.

What happened to:

"any time your data changes, just blow away your view, and re-render it from scratch, that would be so much easier. It would make the structure and amount of code in your app so much simpler ... describe what your view looks like, and then never worry about updating it" - Tom Occhino introducing React in 2013.

But we increasingly have to worry about how, when and where the view is updated. (Suspended, Concurrent mode and Server/Client). The days of glancing at a view and getting a simple, straightforward, and intuitive understanding of what's what, seem gone.

What happened to:

"Only put display logic in your components. I'm not advocating putting all of your model validation code, and fetching, and data access, in components. You might put them into some third party libraries that has some sort of bridge to your components.", as Pete Hunt mentioned when introducing React in 2013. (Did he just not make a strong enough point, so it was forgotten?)

"React renders your UI and responds to events" was how it was introduced. Not: "React is a way to transform and execute all your logic in a hierarchy of declarative markup", as it has devolved into in many cases. "It's all about building reusable components that render themselves into the DOM", as Pete said. Does a <For> or a <Provider> component do that?

It even says so in the current docs:

Components let you split the UI into independent, reusable pieces, and think about each piece in isolation. ... Conceptually, components are like JavaScript functions. They accept arbitrary inputs (called “props”) and return React elements describing what should appear on the screen. https://reactjs.org/docs/components-and-props.html

I feel that in a couple of years people will realise "component" have become a bastardised concept, and while trying to decipher their old codebases, start to think:

What the hell were we thinking, mixing all of these things together into one big pile of tightly coupled concerns? And to put them all into the 'view' of all things??

Not to pick on any library in particular (as the semantic shift has become almost as much a problem in React as any other), but here are how some React-related libraries define a component:

SolidJS Component: "All a component is, is a factory function that generates DOM nodes that are tied to state through function closures of effectful functions.", from SolidJS: Reactivity to Rendering

React Loops Component: "React Loops follows React's model of components as encapsulation of behavior and state.", from react-loops docs

Now compare those to the original definition in React:

"React Component: A highly cohesive building block for UIs loosely coupled with other components." as Pete Hunt defined it when introducing React in 2013

Now, there are subtle but important differences to how React and SolidJS does rendering, so a different definition in SolidJS is to some extent justified.

But could we at least:

  • name same things the same, and different things different?
  • make different things look different? (not using the same kind of JSX tags to denote very dissimilar concepts)

We've moved away from separating the view based on structure (HTML), styling (CSS) and behavior (JS) - which was a good move. Since Components are a better and more coherent encapsulation and abstraction of the UI building blocks. But we've landed over in the ditch on the other side of the road: Putting all sorts of behavior into the same JSX-denoted structure as Components.

I guess if you give people a powerful tool, and freedom without clear direction, they will abuse it, and you will see all possible permutations of it. When you can make everything a nicely declarative component, everything typically gets made into one.

But with every step, we are gradually losing JSX's familiarity with HTML. Which was arguably one of the reasons why it was made, or at least how JSX is understood ([2][3][4]).

Jumping into any codebase, or quickly glancing at markup to know what it does, thus becomes unnecessarily unintuitive.

Can we start separating concerns a little bit more again?
Please?

Ideally keeping components for visual things only. But if not possible, at least stowing those logical components into some part of the app (up in app.js, or down in a localised wrapper component) where they don't litter otherwise hopefully clean component views.

Discussion (25)

Collapse
ryansolid profile image
Ryan Carniato • Edited on

Bear with me a moment as I think this will make sense by the end. Before using component architectures I used MVVM (Model View View Model), which was basically an evolution of MVC.

Within the first couple of years of moving to primarily client-side from the server, it became obvious that MVC was missing a vital thing: the ability to handle state. The earliest client-side frameworks were mostly MVC and they fought with the singleton nature of controllers. They introduced things like $scope, and eventually added new constructs turning MVC into MVC$%#@. This mismatch pretty much was the motivator of the move from Angular.js to Angular 2, Ember 1 to Ember 2 etc.. Basically, the stateless MVC architecture was the wrong one for the client.

Before Components became a thing there was another model though, MVVM, which made View Models instances instead of singletons. This pretty much solved the problem and still keep the separation. However, there was a problem. It was ambiguous whether the View Model should be closer to the View or to the Model. What I mean by that is once you started using the model in multiple places there was a temptation to re-use the View Model as well since there was no necessary tie to the View. This was a pretty bad pattern though since as view models in different locations had new requirements they tended to bloat and it created this second tier of indirection. The way we solved this tie it to the View, it can have re-usability via aggregation on features but not on models. We had landed at the Component model anyway. And really landed at the current Hooks model too back in 2014.

Managing state pollutes everything.


I do think it's good to point out the original React material since I'm not going to lie, I was never sold. I thought it wasn't a realistic expectation. Honestly, I found it infuriating nonsense. It took me years to formulate it properly but something never sat right with me React's launch. I knew in my bones it didn't quite add up.

The declarative view parts weren't that different than their predecessors, they just recognized the separation of view and view model caused in many cases unnecessary complications. And this "Component" model was a good thing as it formalized the solution to what I was talking about above. So the difference wasn't there.

The other big contribution was the Virtual DOM, which as Tom Occhino mention has that benefit in the naive sense. However, they were benchmarking against Angular.js etc... A naive Virtual DOM implementation while more performant than blowing out the DOM each time is still not particularly efficient. What we've seen React do in the past few years is decide they can't just be complacent here anymore.

The initial release didn't provide a real state solution, no handling of Async etc... So even if React wasn't doing it the end-user was. Finding the perfect store technology, the perfect abstraction. And the first couple of years the benchmarks were all geared to cases VDOM libraries benefitted from. Large diffed data snapshots and stock ticker demos.

At a certain point, the team realized that no amount of performance tuning of the library was going to correct the really bad bottlenecks people were creating. Shaving a few kilobytes or shaving a few frames was not going to make a big enough impact. But how could they possibly take control of the state, make the end-user code more understood and optimal? Take a page from reactive systems and give the end-user primitives. Yes, the very same reactive systems they condemned on launch. The ones where you needed to be aware of updates. I'm not saying they saught or even looked at reactive libraries, simply they arrived at a very similar place.

This isn't to say you can't have separate model data. That element hasn't changed but we now declaratively describe our updates instead of act like it will just sort itself. Declarative descriptions don't mean chains of imperative code so they can be reasoned about similar to markup. The original React model was too simple at times. It wasn't a real thing. Ugly shouldComponentUpdate functions, hacked forced updates. They just hid it a bit longer, and no one cared to lift the covers before until the React Team themselves saw these patterns didn't allow for the growth they wanted.


There isn't a templating language that doesn't have some mechanism to handle conditionals or lists. Once you want to reuse markup you have control flow. {#each} v-for, <for>, list.map or whatever. This is just a reality of things. It's just a necessary piece. From a declarative standpoint, an if or for statement are not quite the same as their imperative standpoint. It's more like a flow chart, a circuit. This is not a top to bottom thing but a branch point. I understand in React they are the same things when transpiled, but conceptually in templates that's what they represent.

Suspense is basically an async version of conditionals. Error Boundaries are similar as well. They do have elements conceptually of Context as well but they still ultimately govern what is shown. In fact, there are many reasons you might want the custom ability to control flow. So I see zero contradiction with control flow Components. They define what is visually there. Choose your syntax but this is something all template languages share.

The second category Provider, Context, Query, are all conceptually context-based. These are components that use the hierarchy of the tree to do Dependency Injection. This is more interesting since these could be handled outside of the markup tree, but only reasonably via Composition (or inheritance). And to be fair libraries used to use composed wrappers for this thing, but React decided it was easier to use Components than to introduce a different mechanism. I'm not a huge fan of Context Consumer components like Query but the Provider side which has a hierarchical significance I think it is much easier to visualize this way. We had the wrappers before but this for me (and I guess felt instantly cleaner). I'm not sure if there is anything ambiguous about this.

If anything this plays into the piece-wise localized message the current Docs support. The key is slicing things horizontal instead of vertical. They are all just components each with its own defined purpose. According to the spec JSX is just an XML in JS syntax. I know people explaining things have taken their own liberties in finding ways to easiest explain things, but I don't think there is a contradiction there. If you look at things like Polymer (Google's Web Component framework) they done things arguably more egregious in this area and their components are actual DOM elements.


All this being said you can mostly just not. You can pull your state into top-level global stores, and outside of basic control flows for templating (in what syntax your framework uses) stick to mostly HTML. You can more or less get that sort of model separate feel. It does push the complexity into the store, but something well structured like Redux or State Machines probably match pretty well to this. If you solve state management you remove the need for most of this.

However, there are benefits around modularity to extreme co-location. The ability to look at each part separately is just a first benefit. It also let's different teams easier work in the same code base without stepping on each other's work as much. If done right makes for a different sort of refactor story where you aren't looking across files to change things.

Things have progressed this way deliberately by the framework authors which is why I don't see "freedom without clear direction". It is very intentional where we are at. So what is more interesting to me is what part of this immediately doesn't sit right for you?

What I've picked out here is:

  • Need to consider update model
  • Code readability at a glance
  • Concern for tight coupling

The update model I believe I've addressed above. As for the other 2.

Well, readability at a glance is an interesting one. I'd be on the side that semantically named components that I don't know what they do arguably be an improvement over the other extreme of div soup. If anything I feel I have a better idea what the markup does. However, once you start breaking things small enough the big picture is always harder to see. VDOM update model being tied to Components sort of takes it out of your hands regardless.

Being an API of sorts it does introduce new concepts. I'm not going to pretend I'm onboard with React Server Components as of yet, but it's not off the table. Unless you are talking something like Styled Components there aren't that many intermediate wrappers. Providers generally at App entry and a few boundary components around transition points.

As for tight coupling that's the idea.. wait what? Yes, re-usability is oversold and hasn't really worked well except for the dumbest leaf view components. You can also re-use behaviors, but the combination of domain behaviors and views have this specificity that doesn't often translate. For a while, there was a pattern in React to make everything Smart and Dumb component pairs for re-usability. I'm pretty sure almost 0% of them actually got re-used. Localized coupling is fine as long as it doesn't tie into the whole world. In all cases whether separated or together you need to horizontal slice or you risk too much.

The real value of Components is to toss them out when the time arrives. Set boundary contracts so you can dispose of them with ease. Now I'm not simply supporting <ChartContainer> and <UnconditionalChartContainer>, as that extreme is ridiculous. However, in practice, I can think of so many times that would have saved so much pain.

I think if we take a stance that we are designing this one re-usable super component we can sometimes cause a level of complexity that is hard to unwind. Ironically the work you do to add more complexity to a component tends to need to be undone if you ever split it, returning closer to its initial state. Some more zealous on the React side might say that is a reason to always create new components. But that exacerbates other problems (need for context/prop drilling). I've made it a goal of my frameworks to allow building with re-factorability in mind so component boundaries aren't dictated by the framework. So we are not prematurely creating boundaries (one of the most painful refactoring jobs) nor incurring this refactor cost. See this chain of tweets on the work we are doing with Marko.

Anyway, that is my perspective on this.

Collapse
redbar0n profile image
Magne Author • Edited on

I have updated the article a bit, with some additions and clarifications. Importantly: I did not mean that separating concerns by technology (HTML vs JS vs CSS) is the right way to go about it. I'm fully on board with componentization, I just want the componentization to be clear.

I appreciate the history of how we got here: from MVC, MVVM, to the Flux architecture, to React, and Components.

So I see zero contradiction with control flow Components. They define what is visually there. Choose your syntax but this is something all template languages share.

But they don’t really represent parts of the UI. They don’t denote WHAT is there, but HOW (something else) is there. That’s why I call “control-flow components” for logic-posing-as-markup, or imperative-posing-as-declarative constructs.

I like to think of Components as encapsulating UI structure, behavior (perhaps even state), and styling (ideally, too), based on a single unifying concern. It’s like the atomic building blocks that make up a UI. Like the example from the JSX specification at the top shows. I do believe that’s how they were originally envisioned, as I’ve tried to show evidence of in the article. That was, before parts of the React team started taking shortcuts[1], setting a bad precedent…

I would much less have a complaint here if we were able to abide by what I believe are 2 fundamental principles of good UX and DX (and human experience, in general):


  • name same things the same, and different things different.
  • make different things look different.

The problem I see is that so many fundamentally different things get denoted in the exact same manner (JSX <> tags) and even called the same ("Components"). See the varying definitions of Components I added to the article.

I think the power of React came from it trying very hard not to be a templating language in its own right, but let JS be the language, and the template be the template. Like you say “According to the spec JSX is just an XML in JS syntax”. The React team even denounced templating languages, because:

That’s why it makes so much more sense to having the JS-context as default, instead of the markup/template context as default. JS-with-HTML is superior to HTML-with-JS. Because developers get "the full expressiveness of JavaScript", as Evan You put it.

It is also why templating language constructs like Vue's v-for, Ember's or Svelte's {#each}, or Angular's *ngFor directives, represent an anti-pattern. You begin with a few such concepts, but over time additional language constructs like (for-of, for-in, do-while, while) will be requested... We can already see it in react-for, react-condition, and babel-plugin-jsx-control-statements... Developer whims are unpredictable, and they all like to work in a powerful language that allows them to program behavior in the way they are used to. After all, JS is control flow.

So why not let developers use plain old JS?

(And additionally get the ECMAScript updates for free?)

The second category Provider, Context, Query, are all conceptually context-based. These are components that use the hierarchy of the tree to do Dependency Injection. … React decided it was easier to use Components than to introduce a different mechanism.


Yes, they used the hierarchy, because it was so easily at hand, and it kept the read order top-to-bottom instead of the often counter-intuitive component wrappers which needed to be read right-to-left (since that is how they will be executed; inside-to-outside, as functions). Had the pipe operator (please upvote) already been implemented in JS, I believe the decision may have been different. Since that would have made top-to-bottom read order possible also for wrappers.

The problem as I see it is they compromised the ‘conceptual integrity’ of components, by making something different not only look the same (using <> tags in JSX), but also name it the same (“components”). If something is different, it should look different. So maybe they should have named it differently (f.ex. Controls), and updated the JSX standard with another type of syntax. For example use [ ] [/ ] tags to denote [Controls], to distinguish them from <Components>, like for instance:

[Suspense fallback={<Spinner />}]
  <Dropdown>
    A dropdown list
  </Dropdown>
[/Suspense]
Enter fullscreen mode Exit fullscreen mode

That is, of course, if they should have allowed such Controls in the JSX in the first place, instead of just better supporting using regular JS (perhaps without so much annoying {} for context escaping to JS).

mostly just not

not what? deal with state?

So what is more interesting to me is what part of this immediately doesn't sit right for you?

  • Need to consider update model
  • Code readability at a glance
  • Concern for tight coupling

Actually, it’s primarily the conceptual messiness that doesn’t sit right with me. Components can be such a great and cohesive thing, but instead we’ve got everything-is-a-component, since it allows us to compose it so simply in the hierarchy.

Unless you are talking something like Styled Components there aren't that many intermediate wrappers.

That’s why I feel the problem even more acutely in SolidJS than in React, since there you do tend to frequently come across intermediate wrappers like <For> and <Show>.

I agree that “Localized coupling is fine as long as it doesn't tie into the whole world”. That’s why I’d ideally like it to be contained inside Components.

The key is slicing things horizontal instead of vertical. …
In all cases whether separated or together you need to horizontal slice or you risk too much.

Not sure I understand what you mean by horizontal slice?

I've made it a goal of my frameworks to allow building with re-factorability in mind so component boundaries aren't dictated by the framework.



That’s really great. I think it is maybe the most important goal. I also agree on your stance of composability of components and avoiding premature abstractions (at least insofar as they create indirection); if they make the code more readable/understandable I'm generally for abstractions.

[1] - Like you said, what was easy to do got prioritised from what kept the conceptual integrity intact: “And to be fair libraries used to use composed wrappers for this thing, but React decided it was easier to use Components than to introduce a different mechanism”. That’s what I meant by “without clear direction”: what happened to work got prioritised over how it should work. That’s the React story, since they defined Components, at least for the vast majority of the world. The other framework authors had clear direction, just at the wrong goal (template languages).

Collapse
redbar0n profile image
Magne Author • Edited on

Regarding the point that "template languages invariably end up reimplementing JS language constructs in their markup", since they are underpowered, it's interesting to note that it is also happening to YAML:

news.ycombinator.com/item?id=26271582
blog.earthly.dev/intercal-yaml-and...

"Writing control flow in a config file is like hammering in a screw. It's a useful tool being used for the wrong job."

It even happened to XML, through control-flow logic creeping into XSLT: gist.github.com/JustinPealing/6f61...

I think it represents a general problem: that declarative languages tend to grow imperative, over time, as they are used in situations where the declarations don't cover specific scenarios. Imperativeness creeps in, as programmers are simply trying to get by, without having to wait for language/framework updates to the allowable declarations.

Thread Thread
ryansolid profile image
Ryan Carniato • Edited on

Definitely. The trick is to explicitly denote where these imperative escape hatches are. The most obvious one is event handlers. And perhaps ref. Perhaps it is whenever we enter { } in JSX. But in most cases we are talking about assignment or simple pure expressions which describe a relationship rather than an operation. After all { } accepts expressions not statements, which while not precluding sequential imperative code, pushes more to describing what instead of how.

That being said I don't necessarily view control flows in templating as imperative. I mean I understand that the concept of top to bottom flow and controlling that flow is imperative. So maybe we have a name mismatch. But in terms of declarative view language it's more like electric switches. Like when you look at things like digital circuits and gates. You are literally controlling the flow but it is reflection of state and not some in order execution of instructions. When state updates in a circuit while in reality there is a propagation time, simple models work under the premise all changes applied at the same time. The whole clock rising/falling edge thing. It isn't "this then this". It just is.

This is no more imperative than saying this attribute is bound to the value in this variable. It's a description of what the View should look like and not how we actually accomplish it.

Thread Thread
redbar0n profile image
Magne Author

I think I understand your point.

But in terms of declarative view language it's more like electric switches. Like when you look at things like digital circuits and gates. You are literally controlling the flow but it is reflection of state and not some in order execution of instructions.

But isn't logic gates the very definition of imperative control-flow logic? I'm not sure there is an essential difference between imperative as either 'do-this-then-that' and a setup of switches ala 'when-this-then-that'. In the first instance "you" are doing it. In the second instance you are "declaring" that the system should do it.
But in both cases, it is specifying behavior, not outcome.

The difference you're alluding to seems to be in the locus-of-control: who is initiating the flow that is being controlled. Are "you" directly controlling the system, or have you set up the system in such a way that it is controlled by some configuration when it is executed? I think the latter is just a deferred version of the former. It's just as imperative. If imperative means that you specify specific system behavior (the 'how', instead of the 'what'; i.e. the process of displaying a desired end-state, instead of a description of a desired end-state). Which I think is most reasonable to argue.

Thread Thread
ryansolid profile image
Ryan Carniato

I guess in my view when-this-then-that isn't quite that. Like if you are binding a title to a div:

<div title={state.title} />
Enter fullscreen mode Exit fullscreen mode

You are describing the div's title is tied to this value. Is this when-this-then-that? How about:

<div title={state.title || "default title"} />
Enter fullscreen mode Exit fullscreen mode

Now if there is no title we are setting a default value. Is this control flow? I don't think so. We are describing a logical relationship, but you could argue it's when-this-then-that. To me this is describing what the data is not how the element is updated.

Move this scenario to an insert.

<div>{props.children}</div>
Enter fullscreen mode Exit fullscreen mode
<div>{props.children || <p>Nothing Here...</p>}</div>
Enter fullscreen mode Exit fullscreen mode

Is this different? You can probably see where I'm going.. Now what if that happens to be a component:

function Placeholder(props) {
  return <div>{props.children || <p>Nothing Here...</p>}</div>
}
Enter fullscreen mode Exit fullscreen mode

So while it is subtle passing config instead of executing actions is giving up the control. While these can be the same thing, they are decoupled in that they can be different. Sort of like how even libraries that don't do "2-way binding" still generally end up wiring 2-way binding (data down, events up). That is still uni-directional flow because the wiring is explicit and not automatic.

All code execution is imperative. So by my definition the goal with declarative syntax is to describe the "what" instead of the "how". If your code resembles actions instead of derivations it feels imperative. This is very subtle difference as you can do both by matter of assignment. So maybe this distinction in the specific might seem meaningly, but I find when you step back the overall result is tangible.

Collapse
peerreynders profile image
peerreynders • Edited on

Don't Make Me Think applies for developers just as well as end-users. That's why technologies that feel familiar, like JSX, have generally won.

I argue that this type of mindset is part of the problem.
"Don't make me think" is about site/app design - ensuring that users will arrive at their ultimate objective with a minimal amount of friction. "Don't make me think" in the developer world isn't a is a drive towards nirvana but towards assembly line work (which is easy and mindless and ultimately automated into oblivion).

Being a developer is about solving problems - primarily to solve a domain problem while secondarily resolving any technological challenges that may be relevant in that particular problem environment. So being a developer is very much about thinking. Some tools support that by reducing the tedium that distracts from focusing on the important issues. Other tools restrict thinking because they force decisions that really need to be made explicitly for that particular problem context (which is why some tools are not a good fit for some problems, irrespective of your level of expertise with those tools).

Familiarity may drive popularity but it often does not lead to technical excellence (i.e. popularity is rarely a reflection of quality or relevance). Technical excellence is achieved by focusing on solving the actual underlying problem rather than inventing new ways to more quickly sweep tough decisions under the rug.

But could we at least: name same things the same, and different things different?

The fallacy here is that component as a context free concept actually ever manifested any unambiguous properties. According to the glossary of The Unified Modelling Language User Guide (1999) a component is:

A physical and replaceable part of a system that conforms to and provides the realization of a set of interfaces.

Note there isn't even a reference to the granularity of a "component".
What people want from components is the "ease" of replacing an analog-era speedometer in an automotive dashboard (which was installed on an assembly line in the first place) - and perhaps the aftermarket modifiability/customizability that is inherent to that design. Meanwhile a "React Component" is nothing like a "Web Component" - and therefore not interchangeable.

We've moved away from separating the view based on structure (HTML), styling (CSS) and behaviour (JS) - which was a good move.

There are tools that make that choice for you but web browsers still make that separation because it is a foundational aspect of the web's approach to fault tolerance and resilience in the face of The Fallacies of Distributed Computing. Conceptually the component model is attractive because it can limit the the amount of information that you have to "keep in your head" at any one time - but that is simply not the way a browser deals with the assets it processes. The component model is for the benefit of the human designer/developer - not the technology (browser) that consumes it.

The Real Cost of UI Components:

Rich Harris, the author of Svelte, made the claim that “Frameworks aren’t there to organize your code, but to organize your mind”. I feel this way about Components. There are always going to be boundaries and modules to package up to keep code encapsulated and re-usable. Those boundaries are always going to have a cost, so I believe we are best served to optimize for those boundaries rather than introducing our own.
...
I think Components should vanish in the same way as Frameworks.

Going back to the analog speedometer analogy - even if all the parts were made by the same manufacturer some very different technologies were used to manufacture that component. Meanwhile a web browser doesn't deal in "components" as "raw materials" - in order of importance it deals with:

  • HTML (What to put on the screen)
  • CSS (How it appears on the screen)
  • JS (How it reacts in response to interaction)

i.e. there are no runtime benefits to component boundaries in the browser. Ideally the browser should receive in order:

  1. All the HTML necessary for the first client render (perhaps asynchronously in parts - Island Architecture).
  2. At minimum the CSS to support that first render (but perhaps more).
  3. And last the JS necessary to make the render interactive - by hooking into the DOM as it was rendered by the HTML rather than creating it from scratch.

Ultimately this points to tooling where "components" only exist at design time (if at all), not runtime. This requires some kind of design time compiler.

To my knowledge Marko.js is the only stack that is currently heading in that direction. Given their limited resources it makes sense that they stick to an isomorphic solution (JS & node JS). In order to support greater (and more performant) choices on the server side in the long term I would like to see the development of a programming language agnostic design-time templating solution that can transform page (or island) templates to host language functions that given the necessary data will render the resulting HTML near-instantaneously.

In conclusion: "visual components" may not be all they're cracked up to be even if they seem to be the popular choice in web development right now.

Collapse
redbar0n profile image
Magne Author • Edited on

So being a developer is very much about thinking. Some tools support that by reducing the tedium that distracts from focusing on the important issues.

Sure. And with "Don't make me think", I'm arguing for reducing that tedium, to improve the DX. The point is to rely on a developer's intuition, in cases where the problem cannot be automated away. If it can be automated away, then sure, all the better.

The fallacy here is that component as a context free concept actually ever manifested any unambiguous properties. According to the glossary of The Unified Modelling Language User Guide (1999) a component is:
"A physical and replaceable part of a system that conforms to and provides the realization of a set of interfaces."

Sure. In general parlance a Component can mean just about anything. I am addressing the use in a web development context, specifically with the original intention behind "Component" as introduced by React (as I've tried to evidence in my article). "React Components" and "Components" that are derived from, or attempts to bear similarity to, React Components, is what I'm talking about.

The component model is for the benefit of the human designer/developer - not the technology (browser) that consumes it.

Completely agree. I am simply arguing for the consistency in usage of the first. As I see it, diluting the concept of a (React-style) "Component" is reducing the benefit to you as a human designer/developer to "limit the the amount of information that you have to 'keep in your head'", as you said. I am seeing "Component" as regressing from a strong (and potentially even stronger) abstraction, to a leaky one.

tooling where "components" only exist at design time (if at all), not runtime.

Completely on board with that, if it makes runtime faster and better.

Collapse
peerreynders profile image
peerreynders • Edited on

I am addressing the use in a web development context, specifically with the original intention behind "Component" as introduced by React (as I've tried to evidence in my article).

Yes - but your article is also based on the premise that the concept of a "React Component" is desirable in the web development context - likely due to its perceived popularity - and the second part of my comment was starting to challenge that. I think once you consider the nature of the web as a distributed system and the way browsers actually work (and the full range of capabilities they offer) the notion of a "React Component" (as it is typically implemented) is not ideal for operation on the web. Just because it's popular doesn't mean it's optimal or perhaps even appropriate.

 // Using JSX to express UI components.
var dropdown =
  <Dropdown>
    A dropdown list
    <Menu>
      <MenuItem>Do Something</MenuItem>
      <MenuItem>Do Something Fun!</MenuItem>
      <MenuItem>Do Something Else</MenuItem>
    </Menu>
  </Dropdown>;

render(dropdown);
Enter fullscreen mode Exit fullscreen mode

Over four years ago I looked for the first time more seriously at React. What immediately struck me as odd:

Why are the components organized as a tree?

Of course, an implementation detail was leaking into the design - as a component will ultimately render to a DOM fragment (well, fragment of React Elements), each component is bound to its rendering position within the page (or app).

Now when assembling micro-components from nano-components the owner-ownee communication is often sufficient to make things work but the higher you go up in component granularity the more limiting communications within the component tree becomes - as evidenced by the groaning about "prop-drilling", Redux being included by default, React Context becoming the app's information bus, etc.

However because JSX "looks just like markup" people quickly accepted the "JSX component tree" as an intentional, "by design" feature - rather than realizing that it is a terrible way to organize components that may need to interact with one another in very different ways.

Then there is the practice of building React components that "do all the work" - i.e. rather than focusing on all matters DOM (i.e. presentation and interaction events) they take care of actual app logic and even fetch their own data from the server - ignoring all sorts of system design wisdom that lead to approaches like the Hexagonal Architecture.

The root of this particular problem is that despite its claims to the contrary React is a framework - once you call ReactDOM.render() React is in full control of the UI thread (the only thread in JavaScript unless you use web workers) and it's not going to give it back - so the only way to get any work done is inside a React component (or something a React component calls). So the same forces that lead to the creation of God Classes on the back end lead to "God Components" in React applications - only there it seems to be accepted practice rather than being identified as an anti-pattern. Now hooks may make some components seem less "god-like" simply because some functionality has now been repackaged as a hook - but this isn't a standard refactoring into a function, closure, class or module but a hook which is a React specific (and supported) code/logic organization mechanism.

Ultimately these "God Components" ignore GUI architecture insights that lead to the Humble View (2006) which later gave rise to the Segregated DOM (2014: Keeping jQuery in Check).


The actual "components" in the web development context are rather unexciting.

The HTML fragment (partial)

Just a chunk of markup containing some contextually related data structured for presentation to the viewer possibly exposing some event sites. In terms of code there is the distinction between the actual markup and the data that is placed within it - ultimately leading to the notion of a template. While it is necessary to allow for conditional and repeated data, in general "logic" should be avoided (Rule of Least Power vs. The Case Against Logic-less Templates).

The CSS Block

While a block can align with an HTML fragment( component) it doesn't have to; it could just as easily target some aspect inside multiple distinct HTML fragment( component)s or style a particular aggregate of various HTML fragment( component)s. Also blocks come in two flavors:

  • structure
  • skin

Structural blocks tend to align with HTML fragment( component) boundaries as they give them structure (spacing etc.). Without a skin the "look" of the block will be based on the current global styles (which may be the exact "look" that is needed). Applying a "skin" is akin to snapping a face plate onto a gaming console.

The JS function, class, module etc.

Whatever it takes to add the required interactivity to the relevant section of the DOM created from the server rendered HTML - perhaps with the intent to later replace it with client rendered DOM - but only when and if required. One interesting way of JS taking control of server rendered DOM is Andrea Giammarchi's regularElements. Again the JS component boundary may align with a HTML fragment( component) boundary but something rendered on the server side from a single data aggregate could on the client side easily be controlled by various client-side JS components.

The important thing to notice is that component boundaries of the various aspects don't always align.

Now something like this can already be managed manually today - though it would be tedious as hell. Ultimately design time tooling could help to weave all these elements together in a coherent fashion - but it is conceivable that Content/Markup, Visual Design and Interaction will be managed as separate aspects (i.e. components) rather than one unified UI component in order to optimize the boundaries of each aspect.

The other issue with contemporary runtime components is the conflation of server and client logic to support SSR inside the same component leading to accidental complexity.

Post-compile time there should be two separate artifacts:

  • server artifact - simple from the perspective that it only needs to render HTML and it should be free to acquire the required data any way the server deems fit (for performance reasons) - i.e. it is not coupled to the client's method of acquiring data.
  • client artifact - only deals with client side concerns - no "mixed-in" logic to support SSR.

How that separation is handled at design time depends on what (and how) "components" are being managed at design time.


When you express the sentiment that:

I feel that putting all sorts of logical components into the rendering is abusing the original idea of components.

you may simply be finally realizing the real limitations behind the React component model.

Thread Thread
redbar0n profile image
Magne Author • Edited on

Great insights, thanks for sharing!

It took me a while to process it all, read up on the references you shared, and reflect on it.

The references to 'The Rule of Least Power' and 'The Case Against Logic-Less Templates' were particularly elucidating.

You mention, very interestingly:

Why are the components organized as a tree?
Of course, an implementation detail was leaking into the design

What do you think would be the solution to this? Is there a non-tree structure that works, and doesn't become the wild west?

Thread Thread
peerreynders profile image
peerreynders • Edited on

Is there a non-tree structure that works, and doesn't become the wild west?

Segregated DOM/Keeping jQuery in Check (which allude to the Humble Dialog) essentially boil down to keeping your "view components" as thin possible.

Just enough logic to "hook into" the DOM from the browser-parsed, server-sent HTML, to modify that area of the DOM and some means to "rendezvous" with the application component that sources the data to display and sinks the interaction commands. This way the "application components" (including state management) are free to organize around the necessary communication patterns - not their visual organization on the page.

I argue that the whole Presentational and Container Components discussion in React is related to this - however the concept wasn't taken to its logical conclusion: only the (dumb) presentation components belong in the React component tree - the "application" belongs somewhere else entirely.

This glitch attempts to leverage the organizing principles of the "actual components in web development":

  • disclosure-toggle.js - JS code that takes control of the disclosure-toggle HTML fragment with the help of Andrea Giammarchi's regular-elements (in main.js). Hypothetically this could manipulate the DOM within the fragment especially via template cloning or using something like Nunjucks or lit-html (similarly hyperHTML) but it's just not necessary in this case. Similarly it could "connect" to an "application component" to act as a data source and command sink - but this is just a simple example. This "component" is organized as an ES module (rather than a class) which works fairly well with today's bundlers.
  • disclosure-toggle HTML fragment - server-rendered (or generated), itself composed of the link-button and panel HTML fragments which are styled by link-button.css and panel.css (note that the selectors have a c- prefix identifying them as belonging to the CSS component namespace, o- identifies "objects", js- javascript hooks (don't use data attributes) which are not to be used for styling) respectively.

As I said before, tooling could alleviate some of the tedium of weaving all these elements together.

Thread Thread
redbar0n profile image
Magne Author

I agree that the view components should be as thin as possible. From what I can see, the hooks scenario is exactly what React hooks attempts to solve. The freeing of the communication between components seems what React Context, Redux and Recoil are solving. Yes, the former is still in the render hierarchy, and the two latter are third party libraries, so it would be nice to not need them.

But isn't it good that React at least gives some organisational structure to the code. Otherwise I imagine, when jumping into a new codebase, there would be components all over the place, communicating in any kind of way. That would make debugging much harder, no?

I'm not sure what the disclosure-toggle example is supposed to show. What is the glitch? Is it that it can set the disclosure/ToS as initially hidden, whereas it otherwise would be open by default?

Thread Thread
peerreynders profile image
peerreynders • Edited on

From what I can see, the hooks scenario is exactly what React hooks attempts to solve.

Ultimately it doesn't free you from React though does it?

Why should the application portion of your client side logic care about React?

React's job is supposed to be confined to managing the VDOM - not to provide or run the infrastructure for your client side application.

But isn't it good that React at least gives some organisational structure to the code.

Structure is good but the right structure is better. Ultimately this is Architecture the Lost Years all over again. In the same manner as all Rails apps looked the same, all React apps tend to look the same but that gives no hint whether or not the final app is actually fit for its intended purpose or maintainable in the long run. Eventually some organizations maintaining Rails sites realized that the default structure was sub-optimal and started to challenge the status quo: Bring clarity to your monolith with Bounded Contexts.

Similarly people are bound to discover sooner or later that having React dictate the structure of their client side application may not always be in their best interest.

What is the glitch?

"Glitch" simply refers to the hosting service "glitch.com".
Clicking on the "two fish" (the Glitch logo) to the right takes you to the project view to view the files.

whereas it otherwise would be open by default?

That is the point of the "progressive disclosure component" as designed by Andy Bell; even when JavaScript fails - for whatever reason - no information or access is lost to the user.

The point of the glitch was to present an extremely simple example of "the actual components on the web".

In React disclosure-toggle.js and the disclosure-toggle HTML fragment would be fused into the same component - because they are part of one and the same "component" right? But this ignores the strengths of the browser which is much better at parsing HTML and generating a DOM from it rather than parsing and executing JS to create the DOM out of thin air.

So the JS and HTML fragment are actually separate "components" which at some point have to come together to form the disclosure-toggle "system" that exhibits the desired interaction behaviour. Similarly link-button and panel are "components" with an HTML and CSS aspect but no JS aspect because that has to be supplied by the containing "component" - in this case disclosure-toggle.js.

A lot of tools that have gained popularity recently have prioritized author-time convenience over end-use fitness which is aggravated by the widespread desire to shift web development more towards the experience of back end, desktop or native development. This ignores the fact that the browser-based environment doesn't have the same tolerance for bloat and needs to be treated as a constrained environment in terms of bandwidth, memory and CPU cycles - especially as mainstream personal computing has shifted from fat core desktop PCs to small core hand held devices connected via increasingly oversubscribed cellular networks.

Obligatory links to:

Thread Thread
redbar0n profile image
Magne Author • Edited on

Re: Limitations of the React component model, and I'd thought you'd like this post from 2015 I came across:

"But one problem I’ve run into with this principle is that the React render tree is bound to the same structure as the DOM tree. The flow of data in React is thus bound to the way CSS and HTML allow you to lay out your application. This basically means that information can only flow from larger rectangles to smaller inner rectangles."

medium.com/@chetcorcos/shindig-rea...

Thread Thread
peerreynders profile image
peerreynders

Thanks.

I also just came across UI as an Afterthought (2019)

Setting aside that it centers around Mobx the core idea is:

Initially, design your state, stores, processes as if you were building a CLI, not a web app.

Not that the idea is new but it's interesting to see it applied to client side application logic.

The result:

Most components will be dumb

... at which point something leaner to render the UI might be appropriate.

Also Complexity: Divide and Conquer! (2017).

Thread Thread
redbar0n profile image
Magne Author

Why are the components organized as a tree?

Of course, an implementation detail was leaking into the design - as a component will ultimately render to a DOM fragment (well, fragment of React Elements), each component is bound to its rendering position within the page (or app).

@peerreynders , I read Christopher Alexander's A City is not a tree, and was reminded of our conversation here, and this quote of yours.

I tweeted some sections of the article, of how it may relate to UI:
twitter.com/magnemg/status/1444350...

I presume you are already familiar with the article. How do you think the concept of a UI as a semi-lattice instead of a tree could be applied to UI? Would it be beneficial, at all?

Thread Thread
peerreynders profile image
peerreynders

I presume you are already familiar with the article.

No I wasn't before now.

I'm of course aware of Christopher Alexander — ever since the release of "Design Patterns Elements of Reusable Object-Oriented Software" in late 1994, which referenced A Pattern Language. Now one thing to keep in mind is that the majority of Alexander's work has to deal with physical constraints — something software doesn't have to deal with unless we're talking about the fallacies of distributed computing. That said in this particular instance he is zeroing in on the constraints imposed by the unavoidable mental limitations of humans in their role as designers.

because they are trapped by a mental habit, perhaps even trapped by the way the mind works

To some degree this aligns with The Real Cost of UI Components:

Rich Harris, the author of Svelte, made the claim that “Frameworks aren’t there to organize your code, but to organize your mind”. I feel this way about Components.

but Alexander's point could be interpreted laterally as: "just because components help to organize your mind doesn't imply they are the correct means of structuring your solution."

In my quote

Why are the components organized as a tree?

I was alluding to the conflation of UI and application responsibilities within the same component instance. The tree organization of markup and by extension DOM is simply a constraint of the medium that semi-structures data for the purpose of visual representation. My criticism of React was that it doesn't necessarily follow that the communication pathways and interrelations for the various parts of the application will be accommodated by that same tree.

So when Alexander asserts that a tree is constraining to the point of constriction (compared to a semi-lattice), I see that as a truism with reference to React. Many applications cannot and will not constrain themselves to lifting state up and prop drilling but instead will use context to break out of React's component tree for out-of-band-communication (just like the additional edges of a semi-lattice). So there are really two things going on here:

  • The visual representation is a tree by virtue of the medium (HTML/DOM) that is being used to represent the data to the user.
  • That tree is not necessarily how the application logic needs/wants to be organized.

In the early 2000's there was a lot of discussion around the impedance mismatch between data models and application logic. I think a similar "impedance mismatch" exist between visual design and client side application logic. Trygve Reenskaug arrived at MVC for a very specific context, subsequently MVC was taken increasingly out of context, inevitably leading to problems. By declaring itself as the "V in MVC" React didn't actually address any of the problems but instead positioned itself to absorb functionality from the other two aspects — for better or worse.


Earlier in this discussion I stated:

Now something like this can already be managed manually today - though it would be tedious as hell.

Here is another attempt at describing what I mean: rendezvous (live on glitch).

Thread Thread
redbar0n profile image
Magne Author • Edited on

I agree.

My criticism of React was that it doesn't necessarily follow that the communication pathways and interrelations for the various parts of the application will be accommodated by that same tree.

Indeed. A UI does not behave as a tree. This is vaguely related, and desparingly funny: xkcd.com/2044/

That tree is not necessarily how the application logic needs/wants to be organized.

I think XState is an interesting alternative to organizing application logic (making behavior declarative). Especially on the front end (though potentially also on the back-end) With React: Possibly to the point of avoiding the props interface altogether for updating state (in favor of only using XState's hook) ...

I even think strictly updating CSS shouldn't be done through props, but rather through the CSSOM (like the CSS-in-JS library Stitches does), to avoid triggering prop changes in (and re-rendering of) the entire React tree.

Since the DOM (and VDOM) should be for markup:

HTML markup is transformed into a Document Object Model (DOM); CSS markup is transformed into a CSS Object Model (CSSOM). DOM and CSSOM are independent data structures. -- constructing-the-object-model

But I digress.

By declaring itself as the "V in MVC" React didn't actually address any of the problems but instead positioned itself to absorb functionality from the other two aspects — for better or worse.

I definitely agree with this! Components have become ViewControllers, like in Swift. When Components even contain application state, they become ModelViewControllers...

Which, ironically, isn't so far from what Reenskaug envisioned for MVC (as opposed to the layered architecture it became):

"Every little widget on the screen had its own Model, Controller and View." at MVC is not an Architecture.

Although this approach has some drawbacks, especially related to syncing state changes, as noted at the section starting with "Sometimes MVC is applied at the individual widget level ..." in this excellent and visual MVC explainer article.

rendezvous looks interesting. Although I'm not sure I fully grok it...

This allows client side rendering to be split into two distinct and decoupled phases:

  1. render template (UI structure)
  2. bind behaviour (UI behaviour)

The separation between the "render template" and the "bind behavior" reminds me of StimulusJS. But maybe the thing with rendezvous is that it renders HTML in the same way on the server as on the client? Instead of just manipulating HTML on the client, like Stimulus does.

It also starts with the server and then moves to the client
...
On the client side the state initialization data (to support context) and the transfer messages are extracted from the markup and prepared for use as part of the page configuration. Then the binder definitions are loaded to bind application behaviour to the existing (and future) DOM tree.

I am reminded of Qwik with its resumability vs. replayability. How would they compare?

Thread Thread
peerreynders profile image
peerreynders

FYI:

The liabilities of MVC are as follow:

Intimate connection between view and controller. Controller and view are separate but closely-related components, which hinders their individual reuse. It is unlikely that a view would be used without its controller, or vice-versa, with exception of read-only views that share a controller that ignores all input.

Buschmann, Frank et al. "Pattern-Oriented Software Architecture: A System of Patterns Volume 1". Model-View-Controller, p.142, 1996.

The second division, the separation of view and controller, is less important. Indeed the irony is that almost every version of Smalltalk didn't actually make a view/controller separation. The classic example of why you'd want to separate them is to support editable and noneditable behavior, which you can do with one view and two controllers for the two cases, where controllers are strategies [Gang of Four] for the view. In practice most systems have only one controller per view, however, so this separation is usually not done. It has come back into vogue with Web interfaces where it becomes useful for separating the controller and view again.

The fact that most GUI frameworks combine view and controller has led to many misquotations of MVC. The model and the view are obvious, but where's the controller? The common idea is that it sits between the model and view, as in the Application Controller (379) — it doesn't help that the word "controller" is used in both contexts. Whatever the merits of the Application Controller (379), it's a very different beast from an MVC controller.

Fowler, Martin. "Patterns of Enterprise Application Architecture", Model View Controller, pp 331-332, 2003.

Thread Thread
peerreynders profile image
peerreynders • Edited on

I think XState is an interesting alternative to organizing application logic (making behavior declarative).

XState is a tool — or as I like to say product — the general skill behind the product is statecharts.

My personal attitude

  • General skills tend to depreciate slowly so they are typically a valuable investment.
  • Product skills depreciate quickly so they are the "cost of doing business" and should be minimized.

XState is valuable because it enables the application of statecharts. But as wide as an applicability statecharts may have, they don't solve every problem and for many problems they are overkill especially when yet-another-library has to be pulled in.

Also don't pursue declarative for it's own sake. When it comes to the web HTML and CSS are declarative by default but JavaScript is not. Creating declarative abstractions in JavaScript always comes at a cost — sometimes it's worth it, sometimes it's not — as always it depends.

I even think strictly updating CSS shouldn't be done through props

Why change CSS at runtime at all? CSS rules are designed to remain static for the lifetime of the page but they only become active when the specified conditions are met. You wanted "declarative" but now when you have it you want to imperatively change it at run time?

Perhaps what is needed is tooling that can weave together the required CSS style sheet at design time — prior to deployment.

Components have become ViewControllers

As I sourced in my other reply, View and Controller have traditionally always been heavily coupled in practice so the V-C separation really only existed in idealized descriptions. As you remark, the real problem starts when application state gets pulled into a UI component.

Which, ironically, isn't so far from what Reenskaug envisioned for MVC

Robert C. Martin's account may not perfectly align with Trygve Reenskaug.
(trygve also happens to be a DCI programming language)


In Smalltalk-76, the forerunner to Smalltalk-80, the idea was to let objects represent some information of interest to the user and also to know how to present this information on the screen and let the user edit it. This very powerful paradigm is the basis of the intuitively pleasing object-oriented user interfaces so popular today.

This concept proved inadequate when I wanted to use Smalltalk-76 to create a system for production control in shipbuilding. The information represented in the system was the production schedule with its activities and resources, and the user would want to see and manipulate it in many different forms: as a network of activities, as a chart showing each activity as a bar along the time axis, and as a business form presenting activity attributes as texts that could be edited.

A natural consequence of this was to tear the original object apart, so that one object represents the information, one is responsible for the presentation and one for capturing input from the user. The first was called the model object, the second was called the view object and the third was called the controller object. This gave the freedom to have many different presentations and input facilities for the same object, and even to have several views of a given model on the screen simultaneously.

The object-oriented, direct manipulation user interface gives the user an illusion of working directly with the apparently concrete information objects. The Model-View-Controller breaks this illusion when the user has several views on the same information object simultaneously. This is fortunately of no concern to the professional planner who is manipulating different views of the same plan even in the manual systems.

Reenskaug, Trygve et al. "Working with objects The OOram Software Engineering Method", 9.3.2 Model-View-Controller, pp.333-334, 1995.


So given that models were shared among several views it makes no sense for the model to be inside a single UI component (view + controller). MVVM introduced the idea of a View Model which could be co-located with the View.

But maybe the thing with rendezvous is that it renders HTML in the same way on the server as on the client?

I've only looked at Stimulus JS briefly. In terms of my write up I would say it focuses only on the variation (client side functionality) because in Rails-land there is no commonality to exploit given that none of the server side code — not even any language-independent templates — can be reused on the client side. So when it comes to markup you have to write the template logic twice (I'm not aware of any tooling that translates a common template spec to server and client code).

The other thing is whenever I look at it, it's so infuriatingly Rails. It makes sense because that is the ecosystem it was created for but at same time it seemed to assert its Rails-ness even at the cost of being less web-ish. In my opinion Andrea Giammarchi's libraries are "of the web" — his work tends to be in alignment with "the platform" even when he doesn't agree with it and does he ever know how to get the most out of it. Meanwhile Rails just wants to be Rails regardless of what the web is doing.

Some of my pet peeves with Stimulus JS:

  • Any instance specific information has to stuffed into the markup. And as Harry Roberts points out:

A common practice is to use data-* attributes as JS hooks, but this is incorrect. data-* attributes, as per the spec, are used to store custom data private to the page or application (emphasis mine). data-* attributes are designed to store data, not be bound to.

data-controller identifies the controller category (class) but not the controller instance, while other data-* conventions litter the markup with "quasi-bindings" (that have nothing to do with the markup's responsibility of structuring content) for the sake of being "declarative". The cost of being declarative is having to inherit from the Controller class which presumably scans the markup at runtime for the data-* binding sites which uncomfortably reminds me of Vue's DOM templates. The Controller is heavily coupled to the markup — how much work is it really to correctly bind with the DOM with a few imperative steps right in the connect instead of the Controller base class doing who knows what.

  • All the controller files are stored under one single controllers folder. Really? I don't put all my cutlery, paring, chef's knives and box cutters in the same drawer just because they are all "knives". Why should I organize my code that way?

Rendevous in essence hijacks custom element-like ideas and mechanisms to late-bind client side behaviour to the DOM — regardless of whether that DOM is the result of parsed server HTML or client side rendering (and either are created with the same template or template function).

How would they compare?

Don't know, haven't looked at it in detail. For the time being from what I have heard Astro sounds worth investigating — the said, the current astro/qwik/remix hype is giving me visions of this - making me wonder if they've gone a bridge too far.

Thread Thread
redbar0n profile image
Magne Author

I agree with your attitude and advice, re: XState and statecharts.

Why change CSS at runtime at all?

To change the theme of the page. Dark mode, for instance.

CSS rules are designed to remain static for the lifetime of the page but they only become active when the specified conditions are met. You wanted "declarative" but now when you have it you want to imperatively change it at run time?

Perhaps what is needed is tooling that can weave together the required CSS style sheet at design time — prior to deployment.

Yes. Stitches.dev is that tool. It allows a declarative way of writing CSS-in-JS at design time, while also weaving it together and changing it for you at run time.

Some of my pet peeves with Stimulus JS:

Thanks for sharing! It's very interesting to read your insights. I wouldn't have thought of that myself. Personally, my issue with Stimulus is just that I fear I'll incorrectly wire things up due to typos. Plus that with all the data-* prefixes, it's visually confusing (I feel that data-hello-target should have been a simple id). So I can't really quickly scan it to get the info I need. The latter is an issue I have with Ionic Framework too, with it's insistence on prefixing all their components with ion-* (when it ought to have been a suffix, imho).

the current astro/qwik/remix hype is giving me visions of this - making me wonder if they've gone a bridge too far.

I agree. I am overwhelmed by just trying to understand and differentiate all the different approaches...

I've suggested to Ryan Carniato that he - or someone equally knowledgeable - should make a standardised comparison diagram of the different approaches we have today:
twitter.com/magnemg/status/1435235...
twitter.com/magnemg/status/1435280...
twitter.com/magnemg/status/1438271...

Thread Thread
peerreynders profile image
peerreynders

To change the theme of the page. Dark mode, for instance.

Perhaps I'm naive but I would have thought that's a job for CSS custom properties i.e. an imperative swap of the design tokens that relate to light/dark.

I wouldn't have thought of that myself.

Don't get me wrong. Rails had a point to employ "convention over configuration" to counter the J2EE XML configuration madness at the time. But if you apply "convention over configuration" dogmatically you end up with an awful lot of convention that you have to keep in your head. It's always a question of balance; hard coding vs configuring vs convention and it's not easy to hit the optimum (which depends on the circumstances).

The biggest problem with the rendezvous example is that static.js — for a full page that would be a monster to work with. So you really would need tooling to map a more developer friendly representation to the machine friendly representation.

I fear I'll incorrectly wire things up due to typos. Plus that with all the data-* prefixes,

Look I completely empathize — I immediately thought: "I wish there was a tool that could verify that all the bindings are set up correctly".

Going back even further, when learning JavaScript I found myself craving the false sense of security that a C#/Java compiler gave me that I at least I got the syntax right. At the time I would have been very receptive to TypeScript (these days — poor ROI — how things change).

Now please indulge a digression here — I'll get to a point soon enough.


Oliver Steele: The IDE Divide

That article differentiates between the

  • Language-Maven
  • Tool-Maven

Given that presumed dichotomy in developer attitudes and my personal reading of the declarations of the vocal minority on social media I'm lead to the conclusion that the currently active generation of developers is immensely skewed towards the "I'll adopt it when the tooling is better" type.

That means that a "good idea" has to survive to the early/late majority stage of the innovation adoption lifecycle.

Innovation Adoption Lifecycle

That makes me wonder: How many "good ideas" die on the "innovators" and "early adopters" hill because the developer population is skewed so much towards the "Tool-Mavens" rather than the "Language-Mavens"?


So if you can make Stimulus JS work then by all means do it!

  • Now in your situation you'll be committed to formulating your templates twice and you'll have to keep them in sync — that's just how it is. However I'd consider adopting something like µhtml for client side templating for the larger partials and I would wrap each template in a (pure) function, passing it all the data it needs to instantiate.

  • Whenever possible use reactive bindings to update values inside the DOM rather than replacing nodes. This is the reason why I wish there was a non-UI core of Solid — for most purposes MobX seems a bit too heavy — I'd like something that is lighter than Redux. I guess one could always roll your own.

  • Keep those (Stimulus JS) controllers as skinny as possible. Their job is fairly narrow:

    • On connect/mount
      • Wire reactive subscriptions for value updates into the DOM and add event listeners for input.
      • If the nested content isn't already present use templates to create it client side (their controller(s) will take care of the rest).
    • On disconnect/unmount
      • Remove event listeners and unsubscribe the reactive bindings.
    • Whatever you do keep actual page application logic out of the (Stimulus JS) controllers.

Remember the aim (of rendezvous) is to have a JavaScript core application that can be effectively micro-tested without:

  • running it in a browser
  • having to spin up a fake DOM.

The templates, controller, event and reactive bindings are tested during integration testing with something like cypress or perhaps Puppeteer. The key is that by this point in time you already have full confidence in your decoupled, micro-tested page application logic.

when it ought to have been a suffix, imho

You have to keep in mind that while most browsers will handle non-standard attributes those attributes will actually invalidate the HTML (which is why those pesky alpine.js x-* attributes are another pet peeve of mine). If you are going to 'invent' attributes on standard HTML elements then they better be data-* attributes. At least Stimulus JS got that right.

Collapse
peerreynders profile image
peerreynders • Edited on

And now for a completely different take (continuation of the UI as an afterthought theme).

Redux maintainer Mark Erikson observed (2018):

I've noted that React devs can often be classified into two groups: those who see the React tree as their app / have a "component-centric" view, and those who view their state / logic as their app / have an "app-centric" view.

Hypothesis: Visual Components require an "app-centric" view.

Kent C. Dodds: Application State Management with React (2020):

React is a state management library

This is a "component-centric" view. Essentially React is the foundation of the client-side app. This represents the mainstream use of React. Eventually the article gets to using Context for distributing state:

function CountProvider(props) {
  const [count, setCount] = React.useState(0)
  const value = React.useMemo(() => [count, setCount], [count])
  return <CountContext.Provider value={value} {...props} />
}
Enter fullscreen mode Exit fullscreen mode

However Sebastian Markbåge points out:

My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation.

via Why React Context is Not a "State Management" Tool .

State management solutions like Redux and Mobx don't pass state through the Provider (like the above example) but a static value that makes it possible to subscribe to any updates of state. By extension this creates the opportunity to inspect the updated state before modifying React component state which would trigger a re-render. The process of state selection is often less computationally expensive than (unnecessary) re-renders.

At the end Application State management with React does suggest Jotai as a potential separate state management solution (a minimalist alternative to Recoil). However in contrast to Proxy (Mobx/Valtio) or Flux-based (Redux/Zustand) state management solutions, Atomic state management can be simply "sprinkled" in-between components - i.e. React is still the center of the universe.

In my view for React components to become predominantly Visual / Presentation / Dumb components, React's role needs to be constrained to managing the UI and therefore "React state" should only be "UI state".

Application state and the effects that manipulate it need to be at the centre of the client-side application - not the UI and much less the framework that implements the UI. The UI is simply the user's window into application state which may trigger effects that modify application state.

Hypothesis: Hooks favour the "component-centric" view

Hooks have been around for two years yet Mobx React integration still relies on the observer HOC with the <Observer> component being to only other alternative. In fact there used to be a useObserver hook which has been deprecated - though there is a suggested workaround:

function useSelector(select) {
  const [selected, setSelected] = useState(select)    
  useEffect(() => autorun(() => setSelected(select())), [])
  return selected;
}

function myComponent({observableOrder}) {
  const latestPrice = useSelector(() => observableOrder.price)
  <h1>{latestPrice}</h1>
}
Enter fullscreen mode Exit fullscreen mode

Redux on the other hand does offer useSelector right out of the box but Mark Erikson wrote an interesting article contrasting the tradeoffs of HOCs and hooks - in particular:

  • HOCs promote writing plain components that receive all data as props, keeping them generic, decoupled, and potentially reusable. The downsides of HOCs are well known: potential name clashes, extra layers in the component tree and DevTools, extremely complex static typing, and edge cases like ref access.
  • Hooks shrink the component tree, promote extracting logic as plain functions, are easier to statically type, and are more easily composable. But, hooks usage does lead towards a stronger coupling with dependencies - not just for React-Redux usage, but any usage of context overall.

Elsewhere he notes:

useSelector, on the other hand, is a hook that is called inside of your own function components. Because of that, useSelector has no way of stopping your component from rendering when the parent component renders!

This is a key performance difference between connect and useSelector. With connect, every connected component acts like PureComponent, and thus acts as a firewall to prevent React's default render behavior from cascading down the entire component tree.

So while it's possible to use hooks to integrate React components with the rest of the core application, hooks seem to actually encourage bolting application fragments onto the UI component (making React the core/foundation of the application). In terms of decoupling, HOCs are superior but hooks are more convenient.

Hypothesis: "Component-centric" is the new Active-Record

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.

React Component: "A component that wraps part of the visual UI, encapsulates state and effects that interact with that state".

Active Record is a codified feature in Rails but has been maligned in many contexts as an anti-pattern:

Active Record has the primary advantage of simplicity. It’s easy to build Active Records, and they are easy to understand. Their primary problem is that they work well only if the Active Record objects correspond directly to the database tables: an isomorphic schema.
If your business logic is complex, you’ll soon want to use your object’s direct relationships, collections, inheritance, and so forth. These don’t map easily onto Active Record, and adding them piecemeal gets very messy.
That’s what will lead you to use Data Mapper instead.

Patterns of Enterprise Application Architecture, p.161

DataMapper for Rails never really got much traction presumably because it significantly increased the "initial-time-to-success" - ActiveRecord was already there and it was easy to get started. Later the application never got complicated enough to warrant DataMapper or development had already progressed beyond the point-of-no-return to adopt DataMapper so maintenance had to grin-and-bear the consequences.

I believe there is a similar dynamic with "component-centric" client-side applications. Using React for application state management is easy in the beginning which leads to React becoming the backbone of the application. This doesn't pose a problem for relatively small applications. However once the pain of "component-centric" design becomes obvious it likely would take a great deal of effort to pivot to "app-centric" design, leading to the existing approach being pushed beyond acceptable limits.

Other References:

Collapse
redbar0n profile image
Magne Author • Edited on

I agree. Thank you for very well reasoned thoughts and thorough backing. As always.

I especially agree with:

React components to become predominantly Visual / Presentation / Dumb components, React's role needs to be constrained to managing the UI and therefore "React state" should only be "UI state".

It's interesting to note that Kent C. Dodds and the community currently separates between two forms of state: UI state and Server Cache.

Whereas where it's currently going is that the Server Cache is not just a cache, but also includes Offline state. The ideal model seems something like AWS Amplify DataStore, where the client app has its own state that it will automatically sync with the server. All the while the developer has a nice and simple API to deal with, and not directly concerned about caching, normalization etc. The only problem is Amplify DataStore's bloated bundle size due to its ties to AWS (Cognito auth esp.). So even though open sourced in principle, it's not really universally useful. It has to do with the server sync component and conflict detection needing a server-side counterpart.

This is attempted being solved by Offix DataStore (previously Offix Client, built on top of Apollo Client). Together with the counterpart Graphback, developed by the same people.

This makes the client effectively a part of a distributed system. With all the complexities that entails...

Markbåge seems to think the distinction / architecture doesn't matter:

I think this also plays into the question of "app" vs "component"-based mindsets. Is React your actual "app"? Or is it "just" the UI layer, with the real app being the logic and data kept outside the component tree? Both valid viewpoints. - Sebastian Markbåge, at twitter.com/acemarke/status/114900...

Whereas we think any such perspective will taint the entire client side app, for better and worse. Plus impedance mismatch of people thinking differently and trying to serve the same ecosystem and use each other's libraries.

As you said:

Application state and the effects that manipulate it need to be at the centre of the client-side application - not the UI and much less the framework that implements the UI.

The funny thing is, isn't this where we started: with MVC on the client? Presumably with dumb views. The problem was two-way data-binding, which React brought the Flux architecture to solve. But maybe it should be updated, so that Flux decouples the state from the View layer?

Like Surma did with react-redux-comlink in that presentation you shared. The funny thing is that Redux kinda enforces this separation of concerns between state and rendering But it has been shown to be way too complex for most people and use cases to be worth it in general.. Maybe there is a simpler way?

Collapse
brucou profile image
brucou • Edited on

I can't agree more with a lot of the premises here. What is happening today of course happened before. We have seen decades ago how HTML was starting to include presentational markup and I am grateful that CSS was born to impede that. Let me quote Håkon Wium Lie, who developed CSS in 1994:

Determining the right abstraction level is an important part of designing a document format. If the abstraction level is high, both the authoring process and the task of formatting the document become more complex. The author must relate to non-visible abstract concepts. The benefit of a high abstraction level is that the content can be reused in many contexts. For example, a headline can be presented in large letters on printed sheets, and with a louder voice in a text-to-speech system.
[…]
Conversely, a low level of abstraction will make the authoring and formatting process easier (up to a point). Authors can use visually oriented WYSIWYG (What You See Is What You Get) tools, and the browser does not have to perform extensive transformations before presenting the document. The drawback of using presentation-oriented document formats is that the content is not easily reusable in other contexts. For example, it can be difficult to make presentation-oriented documents available on a device with a different screen size, or to a visually impaired person.
[…]
The introduction of presentational tags in HTML was a downwards move on the ladder of abstraction. Several of the new elements (e.g., BLINK) were meaningful only for particular output devices (how is blinking text displayed in a text-to-speech system?). The creators of HTML intended it to be usable in many settings but presentational tags threatened device independence, accessibility and content reuse.

Presentational tags crept into HTML for the same reason that components are acquiring more responsibilities or <Provider>, <Query> components appeared. One, it was possible. Second, it was the path of least resistance. You won't create a new language just to show something red right? By the moment you need to do more things, and a new, targeted language seems better, it is already too late to reverse course. This is the principle of incrementalism. Third, it was easy. Easy drives adoption.

Now because CSS is its own thing, it can be used completely independently of HTML and has been used to style web pages, pdfs, native apps, and more. That is what separation of concerns gives. Mastery of the concern. Reusability in different contexts.

In the same way, most components you see in a webapp cannot be reused because the more they do, the less it is likely that any other parts needs the same behavior. The smaller the component, the less concerns, the more reusable. This applies to any software component, be it React component, web component, activex controls, etc.

Just like CSS captures the styling concern, HTML (mostly) describes content. It does not describe how that content evolves over time in response to events, i.e. the application behavior. To do this you need to describe arbitrary computations, and here a Turing-complete language makes sense to address the general case. In specific cases, a DSL may suffice but that is domain-, application-dependent.

So JSX is that Turing-complete language, one trivial transformation away from JavaScript. It is a syntactic trick that makes things easy and triggered faster adoption of vDOM-based frameworks. The <For> tag is a step further in the syntactic tricks that are designed to make stuff look like HTML. As Håkon Wium Lie said, this is putting things that are at different abstraction levels at the same syntactic level (Determining the right abstraction level is important). It is easier (a low level of abstraction will make the authoring easier), it may also be confusing. <For> does not describe any kind of content, like say <title>. But some folks think that developers in general want to entertain the illusion that they are writing some sort of enhanced HTML, when they are actually writing JavaScript. Template languages do things much better as they have syntactic constructions that clearly differentiate what is HTML, what is data/parameters, and what is control flow.

Anyways, yes to separation of concerns, always. Yes to architecture, modules, and first principles. It is not like we just started to write software. There are good practices that are decades old that we regularly seem to forget.