DEV Community

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

Collapse
 
ryansolid profile image
Ryan Carniato • Edited

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

Collapse
 
xialvjun profile image
xialvjun • Edited

The real value of Components is to toss them out when the time arrives.

Yeah, and I believe that good code is code that we programmers can delete without fear.

So tree like code which can be deleted without fear is good code.