DEV Community

What happened to Components being just a visual thing?

Magne on December 31, 2020

I really like when Components are actually just visual components. Like the JSX specification indicated: // Using JSX to express UI components....
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
 
3shain profile image
3Shain

I want to share my two cents here (although I was going to write a blog post on this, but the thoughts are too fragmented and not all questions have get a concrete answer, I need sometime to sort out everything) .Some contents may be off-topic but I think they all lead to the conclusion that Component being a visual thing is considered optimal

I'm only going to talk about Components in context of SPA.


While it's never formally documented anywhere that what consists of a Component, it's easy to conclude a subset of specifications:

  • State self-contained data
    • All the logic (e.g. event handlers) and reactive container reference could be also considered State (which not change often/stay constant). After all function (closures) are data as well.
  • Props data provided by consumers, often referred to as parents.
    • * Some framework makes a distinction of props with events, and they are considered different mechanisms in implementation. But conceptually event is for inversion of control, which could be also achieved by passing callback through props
    • In Angular this is called @Input & @Output. They play the same roles though.
  • Declarative Template/View Conceptually a derivation of State and Props. Although the mechanism of rendering is implemented differently in UI frameworks. (which could be said the most significant difference between UI frameworks)
  • Name/Identity
    • In Angular and early Vue you specify the selector and register the component so that the template engine can look up
    • Or just utilize the object/function identity (e.g. JSX)
  • Fractal (tree) Structure Components can be used inside other components' Template/View. Passing data down through Props.
  • Hierarchical Dependency Injection a.k.a. Context. Another approach to transfer data via view hierarchy.

Although there might be more items in addition for a specific UI framework, I think the list above has captured all the important elements of the ever-vague Component _ concept. Notably Fractal Structure makes _Component tend to be scalable(extending feature == adding components) and decomposable (split complex components into small pieces), both features are good signs for complexity management. But wait, decompose by what? scale by what? There seems to be a fallacy that Components can decompose and scale up our applications. However, concretely it's the visual part being decomposed, instead of the application.

Component is you Application should be argued further.

I affirm Component can not be not a visual thing and the reason is simple: a Component can have zero state, no props but it definitely has a template/view, even if it renders nothing. And the fractal structure completely acts on template/view, the visual part, so does the decomposition, with no doubt.

And this makes me interested that, why haven't this misunderstanding/abusing ever cause a problem? Or the problems always exist but never be realized or taken seriously? But anyway, why does it work?.

I suspect there is actually a homomorphism between the application structure (in particular, the data relationships) and the view hierarchy. It's already known view hierarchy is a tree, although I don't know how the application is organized (the module /data dependency), a tree, a semi-lattice, or just a single big dumb POJO (redux's 'interpretation' of Single Source of Truth, I doubt it), as we impose uni-directional data flow (which prevents circular refs), it's not hard to construct a homomorphism - by lifting state up, and as a consequence, props drill down or DI to rescue.

At "worst", you have trivial homomorphism (every elements map to a single unit), just like how you use Redux (connect all components to a single store)

And isomorphism should be really rare (all the Components exactly match a corresponding feature slice of Application, only if your application is really simple, like a Counter), but it's currently somehow assumed to be the truth, so that people end up refactoring, or concluding some practices like Presentational & Container Component, or ask state management tools for help.

This explains why you are always lifting state up (and form props drilling or DI), and why "always place state close to where it's needed" is a bad idea. I know in many times these consequences are acceptable, but what if you treat UI as an afterthought, these problem just never exists. Although there are few, very few materials introducing how it could be applied, and in fact, there haven't had such a well-packaged solution yet. (I see the mobx demo, it's too naive.). I put the reason in the next section.

As for view hierarchical dependency injection, I used to be all-in on that, but just recently I've changed my mind. The issue is, the dependencies of Component should be exposed, just like you have explicit type information on props. Sadly this is impossible and not able to be improved further. For many frameworks it's technically impossible to provide dependency type inferences (unless you hack into editor or IDE). This makes the usage of dependency-consuming Component magical. Sometime the magic worth a lot, it's always a trade-off. But what about the whole dependency injection not acting on view hierarchical at all,
but completely becoming an external mechanism, constructing High Order Component that get dependencies via closure.

You may completely not be able to get the idea from last sentences above and I admit I should show some concrete demo code.

function* Parent() {
    // what could be a injected dependency? 
    // a reactive store, a encapsulated api
    // or anything
    const globalSotre = yield GlobalStore;
    const httpClient = yield HttpClient;
    const parentState = createReactiveState({...});
    const ChildComponent = yield* Child(); 

    // whatever I'm using a react-like stuff for demostration
    // could be light-weight because we don't need a "full" component
    return ()=><>{/* 
        parent view
        have access to childState and injected dependencies
        */}
        <ChildComponent />
    </>
}

// the yields type gives all the dependency type information
// 

function* Child() {
    const idontknow = yield WhatEver;
    const httpClient = yield HttpClient;
    const childState = createReactiveState({...});

    return ()=><>{/* 
        child view
        have access to childState and injected dependencies
    */}</>
}

const ParentComponent = (function(context){
    let g = Parent(), next = null;
    while(true) {
        const ret = g.next(next?context.find(next):undefined);
        if(ret.done) return ret.value;
        next = ret.value;
    }
})({
    find: (token)=>{/* implement your dependency locator */}
});

render(<ParentComponent/>, document.body);

Enter fullscreen mode Exit fullscreen mode

Although the code doesn't work at all (have some significant holes), it demonstrates some ideas:

  • use generator to implement dependency injection, and get type inference for dependencies, without aware of view hierarchy
  • use reactive state outside component rather than in-component state. you get keep-alive/ off-screen component for free
  • dynamic constructed component (HoC) instead of direct referencing/importing. model and view are orchestrated separately (comparing to template be the only truth of composition)
  • there are actually two way passing data down from Parent to Child, one is component props, another is Child() call. shouldn't props be designed two parts by default? a model part and a view part...
  • the state of Parent and Child are logical at the same level (flatten nested generator call) while they have a view hierarchical order.

I'm working on a library making these ideas more standardized.


Both the application and view structure could be dynamic. In Component you could write conditional or repeated structure, but I have seen few public state management solutions provide such capability.

Some state are long live (esp. global state) but some are transient (local). And not all transient state are UI state. Within Components it's related to reconcile of render process - identifying what's added/removed. Not only the visual elements are added/removed, but also the state, or the Component instances containing the state are constructed/disposed (normally GC takes care of that, but a subscription must be explicit unsubscribed).

I need to mention that the identity of Component is not always the same as the identity of Instance of Component. That's why in some framework you need to specify the key.

By the way recoil & jotai provides atom based API which looks great, until you see atomFamily which suddenly breaks the simplicity - exactly the problem from (subjectively) confused identity. The Atoms have a dynamic structure as well, not because the view is dynamic, but a property inherited from the model.

Now given Both the application and view structure could be dynamic, and we want to separate the two, then where is the reconcile process for application (to be specific, the model, the state regardless of view)? I guess that's one of the reason why component-centric design is hot but UI as an after thought is rarely a easy and convinient practice (you end up using single global store, or create distributed store inside Component which isn't applying the slogan at all).

We are lacking of another abstraction, that provides the dynamicity for application, and probably fractal as well (and DI works on this abstraction instead of view hierarchy), takes the place of Components' role apart from visual stuff.


I used to coin that Component is a one-size-fits-all abstraction, but soon I realize the same reason can be used to blame function/class (and it's in certain degree reasonable? I really desire an explicit distinguishing from pure/side effect functions). One-size-fits-all abstraction not bad though, I prefer primitive abstraction.

Why Component is not a primitive abstraction? Because people are discriminating different Types of Component (not referring to functional/class): like Element / Widgets / Layout / Page etc. Although there is no actual formal specification on this, however, several differences are obvious: you can construct stateful Component with zero props, or pure Component with dozens of configurable props; regardless of state & props, some Components tend to be very stable and re-usable (esp. UI Primitives, Design System) and some are never re-used but volatile. Think of all the variable factor and their possible combinations. All these "minor" details are hidden behind a single word Component.

What if we further constrain the concept of Component? To become a completely visual thing could be a good move. And again we need another abstraction that takes the role of rest.


Ryan wrote a post Components are Pure Overhead on suggesting Components could be completely eliminated on the runtime. I think Components as a mental boundary is still, not optimal.

The Component does have a niche - on encapsulating custom UI (primitive) elements. This kind of Components are usually well-designed, very stable and highly possible being re-used. Most significant point is that they are simple, conceptually simple, visually simple, because they touch no part of your business.

But the rest use cases of Component is not that lovely. Because you start build components for your business goal (the business logic, and the visual layout). These components are rarely reused, and tend to change (high volatility). Now the issue comes: visual decomposition doesn't help you to encapsulate volatility, but make it worse (changes can ripple through props). To solve this, you either try your best not to encapsulate new component (is that really a solution?) or try your best not to use props (all-in DI/Context? or state management with connect HoC). Maybe Components apart from UI Primitives don't need props at all.


Not all the questions/statements I yielded have be answered/proved. (subjectively I think I have already had answers but I feel they are not mature enough yet). The most confusing is that I didn't give any detailed specification of that abstraction. I think I've almost found it, but it need to be simplified and I might realize it has been invented decades ago, just saying

Collapse
 
redbar0n profile image
Magne • Edited

Thank you for a thorough and comprehensive comment. I agree with much of what you’ve said, and must admit that other parts of it (esp. the details) goes above my head.

Maybe Components apart from UI Primitives don't need props at all.

I was reminded of this blog post by Pete Heard, on using components completely without props (aka. Niladic Components):

logicroom.co/blog/upgrade-your-rea...

I haven’t made up my mind about it. @peerreynders also shared it with me. What’s your your opinion on that approach? Is it similar to what you envision? See particularly the referenced CodeSandbox to get the overview of it. It helped a lot.

Collapse
 
peerreynders profile image
peerreynders

I haven’t made up my mind about it.

From what I can see he's decided to make a business out going Uncle Bob on the front end. There was a time where I could have gotten behind that but due to realities like The Cost of JavaScript Frameworks the prescribed approach is likely to make React applications just as heavyweight as Angular ones. That's OK for internal enterprise apps and that's his likely target audience - but I'm sceptical whether that approach will have desirable outcomes on the public web.

And again we need another abstraction that takes the role of rest.

In a hand-wavy way I was kind of thinking something like uhtml/uhtml-ssr for DOM/SSR (templating) and Solid's reactive state (without any DOM/Browser dependencies; ultimately I find MobX just too heavyweight) both integrated but separate libraries, mainly so you can run automated tests of the client app server side without a fake DOM/Web APIs.

I'm only going to talk about Components in context of SPA.

Sure. The caveat there though is that if experiences. Web. frameworks. future. me. turns out to be accurate then Gen 3 has the potential to de-emphasize SPA - if server-first Gen 3 can deliver fast Time to Interactive then the faster "page transition times" after load could become a moot point in favour of SPA for many but the most complex apps.

From that point of view I recommend having a detailed look at how Qwik operates. From what I can tell there really isn't notion of a component in the React or WC sense. It's about just-in-time lazy loaded event handlers (and their dependencies). Configuring its lazy load strategy is going to be reminiscent of performance tuning a database—the user usage data is going to inform what is going into the first critical bundle which is loaded on the first idle. The idea of eager loading the entire client application goes out of the window.

A mixture of minimal optimistic loading and lazy loading the rest could potentially work well over the web when users tend to only use 20% of the application capability.

Thread Thread
 
3shain profile image
3Shain • Edited

I was reminded of this blog post by Pete Heard, on using components completely without props (aka. Niladic Components):
logicroom.co/blog/upgrade-your-rea...

I haven’t made up my mind about it. @peerreynders also shared it with me. What’s your your opinion on that approach? Is it similar to what you envision? See particularly the referenced CodeSandbox to get the overview of it. It helped a lot.

I'm not very sure it's the same idea I've talk about, I think it is. The idea is prevent business logic from being leaked into the view.


Sure. The caveat there though is that if experiences. Web. frameworks. future. me. turns out to be accurate then Gen 3 has the potential to de-emphasize SPA - if server-first Gen 3 can deliver fast Time to Interactive then the faster "page transition times" after load could become a moot point in favour of SPA for many but the most complex apps.

I have seen qwik, really a good job. And I admit Gen 3 will be the true web solutions. Although that's not my concern. I was supposing that even the Gen 2 is not on the right track (maybe I'm on the direction of post-Gen 2?). I think Gen 3 still share the (conceptual) problems I've indicated, they are not gonna be solved by changing from client to server-first.
I'm only concerned with the application, the architecture , if possible, in a ideal world. "The web" is too specific. (I'm not against constraints. constraints imo are the most valuable and powerful tool of programming)

Thread Thread
 
peerreynders profile image
peerreynders • Edited

I'm only concerned with the application, the architecture , if possible, in a ideal world.

But how are you going to justify the view/domain separation to a component die-hard? They are going to ask "what's in it for me?"

"The web" is too specific.

"The web" also has a habit of scuttling good ideas with its unique challenges (unless they were carefully considered up front).


In a way you have landed on the Humble Dialog (or Humble Object) from 20 years ago.

I agree with your analysis that it makes no sense to organize the "application" in terms of the flow of elements in the document tree; I think the heavy use of Context and external state managers in React applications is testament to that.

That being said I think that it may be necessary to avoid the term/concept Component altogether when describing a new architecture because the intended audience will most likely use React Component as a reference and assume its particular compositional qualities (and capabilities with regards to view/application state).

Domain logic partitions around domain capabilities while the UI partitions around UX concerns - both of which may or may not align.


Another aspect is that developers of component-centric designs may just favour bottom-up design

Quote:
"In order to get IntelliSense to work correctly, bottom-up programming is best. IntelliSense wants every class, every method, every property, every field, every method parameter, every local variable properly defined before you refer to it. If that’s not the case, then IntelliSense will try to correct what you’re typing by using something that has been defined, and which is probably just plain wrong."

By being concerned about "application architecture" (top-down) you're in the app-centric camp—while meaningful boundaries still exist, component boundaries aren't the salient concern.


I assume you are familiar with Martin Fowler's GUI Architectures Page and André Staltz's Unidirectional User Interface Architectures.

Thread Thread
 
redbar0n profile image
Magne

I'm not very sure it's the same idea I've talk about, I think it is. The idea is prevent business logic from being leaked into the view.

Yes, I think it’s precisely that same idea. Pete Heard talks a lot about separating out all business logic, so it is testable, and the UI being an afterthought (even one step further than what MobX does).

Collapse
 
peerreynders profile image
peerreynders • Edited

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

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

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

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

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

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?

Collapse
 
redbar0n profile image
Magne • Edited

Here’s a good illustration of how it can go wrong:

Image description

x.com/trashh_dev/status/1719800548...

Where did we all take the wrong turn…?🤔

Collapse
 
redbar0n profile image
Magne • Edited

Case in point:

When reading JSX, I feel confortable to imagine a 1-to-1 match with the UI. I can easily navigate thought the component tree and map with the reality.

Image description

sancho.dev/blog/tailwind-and-desig...

Collapse
 
redbar0n profile image
Magne

My suspicion towards the complexity introduced around where Server Components are executed...:

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?"

...seems to have just been validated:

"While we’re excited for the future of RSC long term, as a critical dependency, it presented a number of challenges that became performance risks for our developers, and bottlenecks for our progress."
...
"We’ve found that keeping a clear separation around where your server is doing the work, and worrying less about how your server and client components might mix together, has resulted in a simpler developer experience that’s less prone to mistakes." -- Shopify Engineering, on 2022-10-31 in What's next for Hydrogen: Data Loading

Collapse
 
redbar0n profile image
Magne

"an application’s UI as a pure function of application state." - Guillermo Rauch on Pure UI

As opposed to:

An application's UI as functions that control and determine state.
(I'm looking at you, React Router).

Related:

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

Collapse
 
carloslfu profile image
Carlos Galarza

Great article! @redbar0n

Collapse
 
brucou profile image
brucou • Edited

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.

Collapse
 
peerreynders profile image
peerreynders • Edited

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

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?