DEV Community

Discussion on: What happened to Components being just a visual thing?

Collapse
 
redbar0n profile image
Magne • Edited

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 • Edited

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

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

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.