DEV Community

Magne
Magne

Posted on • Updated on

What happened to Components being just a visual thing?

I really like when Components are actually just visual components.

Like the JSX specification indicated:

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

render(dropdown);
Enter fullscreen mode Exit fullscreen mode

It makes me able to look at a page in the browser, and go into the code and intuitively just pattern-match what I saw in the browser to the structure in the markup and immediately see where I have to make the change.

But now.. I find myself increasingly having to actually cognitively 'parse' the markup/JSX before I can find my point of inflection. (Don't Make Me Think applies for developers just as well as end-users. That's why technologies that feel familiar, like JSX, have generally won.)

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

The original idea was:

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

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

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

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

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

It makes me sad.

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

What happened to:

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

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

What happened to:

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

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

It even says so in the current docs:

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

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

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

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

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

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

Now compare those to the original definition in React:

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

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

But could we at least:

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

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

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

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

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

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

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

Discussion (52)

Collapse
ryansolid profile image
Ryan Carniato • Edited on

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

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

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

Managing state pollutes everything.


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

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

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

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

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

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


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

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

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

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


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

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

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

What I've picked out here is:

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

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

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

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

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

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

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

Anyway, that is my perspective on this.

Collapse
redbar0n profile image
Magne Author • Edited on

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

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

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

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

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

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


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

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

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

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

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

So why not let developers use plain old JS?

(And additionally get the ECMAScript updates for free?)

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


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

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

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

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

mostly just not

not what? deal with state?

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

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

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

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

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

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

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

Not sure I understand what you mean by horizontal slice?

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



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

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

Collapse
redbar0n profile image
Magne Author • Edited on

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

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

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

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

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

Thread Thread
ryansolid profile image
Ryan Carniato • Edited on

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

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

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

Thread Thread
redbar0n profile image
Magne Author

I think I understand your point.

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

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

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

Thread Thread
ryansolid profile image
Ryan Carniato

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

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

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

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

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

Move this scenario to an insert.

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

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

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

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

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

Collapse
xialvjun profile image
xialvjun • Edited on

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.

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 Author

Thank you for a thorough and comprehensive comment. I agree with mich 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 on

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 on

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 Author

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

Thread Thread
peerreynders profile image
peerreynders

In broad strokes he's mapping Clean Architecture to the front end.

I'm still partial to this 2018 "actor inspired" architecture for the front end.

GitHub logo PolymerLabs / actor-boilerplate

A starting point for web apps based on the actor model.

Actor Boilerplate

A starting point for web apps based on the actor model.

Screenshot of a stopwatch web app that uses the Actor model

Chrome Dev Summit 2018 Talk

Architecting Web Apps - Lights, Camera, Action!

We also wrote a series of blog posts with more detail on web development with the actor model:

What is this repository that am I looking at?

This is a very basic web app that uses the actor model. The actor model helps you to break your app’s core logic into small pieces that have to communicate with messages instead of using function calls. Adopting this model has multiple benefits on the web:

  • Yields to browser (it naturally leads to chunked code)
  • Encourages lazy-loading and code splitting
  • Gives you a clear separation of concerns
  • Makes moving code off-main-thread easier
  • Resilience against unexpected long-running tasks
  • Enables multi-modality for web apps

What’s in here?

This boilerplate…





actor-helpers

Helpers to build web applications based on the actor model. These helpers are used in our examples, for which you can find our boilerplate here. We encourage you to read through the boilerplate examples first and then read through the code in this repository.

actor.ts

actor.ts contains a base class implementation for an actor, as well as functions to hookup() and lookup() actors on the web page. For more detailed examples, please check out the in-file documentation.

watchable-message-store.ts

This store is an implementation detail of the messaging system used by hookup() and lookup() to allow actors to communicate with one another. You shouldn't need to interact directly with the message store, but it's here all the same if you do.


License BSD-3-clause

Please note: this is not a Google product.

Too bad it didn't go anywhere … but as usual I digress …

Thread Thread
redbar0n profile image
Magne Author

What do you think about XState, which is actor model inspired state handling often used on the front-end?

Ideally, I imagine it would be nice with Niladic Components (no props), so React could be a dumb View layer, and simply handle all state separately in XState…

Thread Thread
peerreynders profile image
peerreynders • Edited on

See my comment here

Expressed roughly in JavaScript-like syntax an Erlang process is at its simplest

function loop(state) {
  const msgFrom = receive();

  const nextState = (lastState, { type }) => {
    switch type {
      case 'a':
        send(msgTypeA);
        return toStateA(lastState)

      case 'b':
        send(msgTypeB);
        return toStateB(lastState)

     default:
        return lastState
    }
  }(state, msg);

  loop(nextState)
}
Enter fullscreen mode Exit fullscreen mode

Of course JavaScript

  • doesn't implement tail(/last) call optimization
  • isn't pre-emptively scheduled. Typically a process is blocked at a receive() (or any other blocking call) waiting for the next message and even if it isn't the scheduler will suspend it after it consumes it's reductions.

None of this is feasible in single threaded/event loop based JavaScript but it points to the possibility of tying process scheduling to message delivery:

  • A "processing scope" (replacing "actor") only gets processing control when it receives a message (with some exceptions; the message would likely be arrive via a subscription). To be well behaved it has to break up large tasks into phases which it initiates by sending a message to itself at the end of the current phase.
  • To interact with other "processing scopes" is has to send a message. However there is no immediate response (the message goes to the end of the delivery queue). If there is a response it will be received later at which point in time it can be processed (so the processing scope state has to reflect this "in progress interaction" - the equivalent to a "function call (returning a value)" to a separate processing scope).

So the basic functionality needed for a processing scope is a way to receive messages so it has something to do, and a way to send messages so it can collaborate with other processing scopes. For that to work the message router/scheduler needs to support a few things:

  • The entire system is built around postMessage(). For the purpose of discussion lets call a Window or Worker a "processing node".
  • Each node
    • routes messages (i.e. has to manage routing data)
      • messages to the local processing scopes are put on the delivery queue
      • messages to processing scopes managed by another node are immediately posted to the nearest node.
    • schedules processing by
      • delivering a single message to the local processing scope from the head of the delivery queue provided there is enough time left in the current processing slice
      • returning control to the event loop when the current processing slice is exhausted.
        • if the delivery queue isn't empty the next processing slice needs to be scheduled (requestIdleCallback(), setTimeout()).
        • when the delivery queue is empty use queueMicrotask() to start the next processing slice when a message is placed on the delivery queue arriving from another node or a general send (e.g. caused by an input event).

Now a system like this is inherently asynchronous—so in my judgement has little to no chance to be adopted by the JavaScript community given how relieved everybody was when async/await was introduced.

In terms of the PolymerLabs MessageStore I would be concerned that forcing messages through IndexedDB would slow things down unnecessarily.

Is postMessage slow?

But thanks for asking …


it would be nice with Niladic Components (no props)

Note what is going on in his niladic solution:

He is failing to extract the variation of two near identical components in order to preserve their "niladic" property

import './styles.css';
import { useEffect, useState, useCallback } from 'react';
import { external } from './External';

function Value({ update }) {
  const [value, setValue] = useState();
  const onChange = useCallback(
    ({ target: { value } }) => {
      setValue(value);
      update(value);
      external.doFinalCall();
    },
    [update]
  );

  return (
    <span>
      <input value={value} onChange={onChange} />
    </span>
  );
}

const left = (value) => external.left = value;
const right = (value) => external.right = value;

export default function App() {
  const [total, setTotal] = useState(0);

  useEffect(() => {
    external.registerFinalCall(setTotal);
  }, [total]);

  return (
    <div className="App">
      <h1>Totals</h1>
      <Value update={left} />
      <Value update={right} />
      total is {total}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

props will be involved whenever you use a component more than once on the same page to represent separate business entities.

Thread Thread
3shain profile image
3Shain • Edited on

props will be involved whenever you use a component more than once on the same page to represent separate business entities.

As I said before

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.

although there are no real key here but different update props are passed to identify different entities. In certain degree it's the mismatch between UI and application proved again.

and the solution is fairly simple... encapsulate High Order Component

although it's not necessary to involve any architecture decision, I'm placing the kairo's solution here

const [External, ExternalImpl] = createConcreteConcern('External', function*() {
    const [leftValue, setLeft] = yield* State(0);
    const [rightValue, setRight] = yield* State(0);
    return {
        leftValue, rightValue,
        setLeft, setRight
    }
}); // a shortcut of `createConcern` with a default implementation

const Value = UI(function*({value, setValue}:{value:Cell<number>, setValue:Setter<number>}) {
    const onChange = e => setValue(e.target.value);
    return Component((_,$)=>(<span><input value={$(value)} onChange={onChange}/></span>));
});

const App = UI(function*() {
    const {leftValue, rightValue, setLeft, setRight} = yield* External;
    const Left = yield* Include(Value.withProps({value:leftValue, setValue:setLeft}));
    const Right = yield* Include(Value.withProps({value:rightValue, setValue:setRight}));
    return Component((_,$)=>(
    <div className="App">
      <h1>Totals</h1>
      <Left />
      <Right />
      total is {$(leftValue) + $(rightValue)}
    </div>));
});
Enter fullscreen mode Exit fullscreen mode

notably I made Value fully controlled because it's unnecessary to have a local state as there is already a source of truth.

3shain profile image
3Shain • Edited on

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

TBH I haven't thought about this. I thought it's natural to choose the former because it's better architected (hope it's not a bias but can be theoretically proved). Although It's never shown that people will always choose the "better solution in theory" ,but likely the "good solution that fits my problem".

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

(handclaps)


Another aspect is that developers of component-centric designs may just favour bottom-up design
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.

Recently I realised I was finding (or has found) another "Component" abstraction which is different from the current Visual Component (as I really want to preserve the fractal structure). But yeah it might be better to give it another name other than Components.


I did read about the materials you've mentioned. In fact I knew about them from your previous comments and I have to say I've learned a lot. Thank you for your always informative sharing. I hope one day I can show an opinion always with dense external sources like you.

Thread Thread
peerreynders profile image
peerreynders

Although It's never shown that people will always choose the "better solution in theory", but likely the "good solution that fits my problem".

When was the last time you were presented with a counter app like this?

import { createRoot } from 'react-dom';
import { createContext, useContext, useEffect, useState } from 'react';

function makeCountApp(count) {
  const listeners = new Set();

  return {
    increment,
    subscribe,
  };

  function notify(listener) {
    listener(count);
  }

  function notifyListeners() {
    listeners.forEach(notify);
  }

  function increment() {
    count += 1;
    notifyListeners();
  }

  function subscribe(newListener, now = false) {
    listeners.add(newListener);
    if (now) newListener(count);
    return () => {
      listeners.delete(newListener);
    };
  }
}

const AppContext = createContext();

function UI({ initCount }) {
  const [count, setCount] = useState(initCount);
  const app = useContext(AppContext);
  useEffect(() => {
    const update = (newCount) => setCount(newCount);
    return app.subscribe(update);
  }, [app]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={app.increment}>Click me</button>
    </div>
  );
}

function App() {
  const initCount = 0;
  return (
    <AppContext.Provider value={makeCountApp(initCount)}>
      <UI initCount={initCount} />
    </AppContext.Provider>
  );
}

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(<App />);
Enter fullscreen mode Exit fullscreen mode

which I would classify as app-centric; versus this (component-centric):

import { createRoot } from 'react-dom';
import { useCallback, useState } from 'react';

const increment = (value) => value + 1;

function App() {
  const [count, setCount] = useState(0);
  const incrementCount = useCallback(() => {
    setCount(increment);
  }, []);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={incrementCount}>Click me</button>
    </div>
  );
}

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(<App />)
Enter fullscreen mode Exit fullscreen mode

Many people gravitate towards what's easier and expedient in the short term ("the scam") and that is what the "business" often wants—faster time to market. Your don't have to go to apply for loan to go into technical debt, you just do it. Once React was described as the "V in MVC" but from I can tell components (with hooks) are often kitchen sinks combining various UI and application responsibilities-the only mitigating factor being that the complexity is limited to a single UI Element or the complexity is delegated to nested components (reminding me of HMVC). And the culture in the community seems to favour component-centric approaches (e.g. Application State Management with React).

Perhaps if you are going to rewrite the product every two years anyway it doesn't matter that there is no discernible "architecture", however it may also make the rewrite a foregone conclusion.

"Software architecture is those decisions which are both important and hard to change" (Making Architecture Matter). The flip-side is that failing to identify up front what is truly important and what will need to change in the future can still lead to poor architectural decisions.

I did read about the materials you've mentioned.

Native app developers seem to focus on MVVM, MVP(VM) or VIPER these days. 43 years have passed since the introduction of MVC and the refined UI patterns still keep coming. This illustrates that this is a non-trivial problem without a one-size-fits-all solution. I think it's important to be familiar with the various ways this problem can be broken down—that way simple problems can be solved simply, while options (which are overkill otherwise) exist for the more gnarly cases.


FYI: ThoughtWorks Technology Radar: SPA by default:

"Too often, though, we don't see teams making that trade-off analysis, blindly accepting the complexity of SPAs by default even when the business needs don't justify it. Indeed, we've started to notice that many newer developers aren't even aware of an alternative approach, as they've spent their entire career in a framework like React."

Thread Thread
3shain profile image
3Shain • Edited on

When was the last time you were presented with a counter app like this?

I was crafting a library/framework/architecture(still in private yet) and this is how it works:

initially it looks like typical "component-centric"

import { State, __buildAppNotStableYet } from 'kairo';
import { UI, Component } from '@kairo/react';
import React from 'react';
import { createRoot } from 'react-dom';

const increment = (value) => value + 1;

const Counter = UI(function*() {
    const [count, setCount] = yield* State(0);
    const incrementCount = ()=>setCount(increment) 
    return Component((_,$) =>
    <div>
      <p>You clicked {$(count)} times</p>
      <button onClick={incrementCount}>Click me</button>
    </div>);
});
// typeof<Counter> : ImplementationOf<'@kairo/react:UI',DependsOn<'CounterApp'> | WithLifecycle, {}, FunctionComponent<{}>>

const { instance: App, start } = __buildAppNotStableYet(
    Counter.provide(AppImpl) // assembly your application
);
// typeof<App> : FunctionComponent<{}>
start();
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(<App />);
Enter fullscreen mode Exit fullscreen mode

but soon you could refactor it to a more decoupled version (you could say it's "application-centric" coz you can write App and unit test it regardless of UI, the same is true for UI)

import { State, createConcern, Cell, __buildAppNotStableYet } from 'kairo';
import { UI, Component } from '@kairo/react';
import React from 'react';
import { createRoot } from 'react-dom';

const increment = (value) => value + 1;

const App = createConcern<{
    count: Cell<number>,
    increment: ()=>void
}>()('CounterApp'); // a branded interface, also a injection token

const AppImpl = App(function*({initCount}:{initCount:number}) {
    const [count, setCount] = yield* State(initCount);
    const incrementCount = ()=>setCount(increment);
    return {
        count,
        increment: incrementCount
    }
}); // implement the interface (concern)
// typeof<AppImpl>: ImplementationOf<'CounterApp',WithLifecycle,{ initCount: number }, {
//  count: Cell<number>,
//  increment: ()=>void
//>

const Counter = UI(function*() {
    const { count, increment } = yield* App;
    return Component((_,$) =>
    <div>
      <p>You clicked {$(count)} times</p>
      <button onClick={increment}>Click me</button>
    </div>);
});
// typeof<Counter> ImplementationOf<'@kairo/react:UI',DependsOn<'CounterApp'>,{}, FunctionComponent<{}>>

const { instance: AppComponent, start } = __buildAppNotStableYet(
    Counter.provide(AppImpl.withProps({initCount: 0})) // assemble your application
);
// typeof<AppComponent> : FunctionComponent<{}>
start();
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(<AppComponent />);
Enter fullscreen mode Exit fullscreen mode

That's my solution anyway. How is it different from current Component model? I'm arguing current Components are too bloated so everything kairo provided are primitives - do one thing only and it encourage you to decompose things into small concerns (if applicable). For UI (frameworks like react) the (visual) Component should be only visual thing.
One could argue "then everything is coupled with kairo" then I dare say I'm shipping language-level abstractions/primitives (that might be biased).
(and an ultimate goal is to supporting all mainstream UI frameworks(as well as some leaner render coz a full Component is not required), and they are supposed to truly play the role of view layer. maybe even not limited to front-end)

Don't blame that it doesn't focus on web, it's never supposed to be. SPA be abusing doesn't matter that (Visual) Component-based is a sub-optimal architecture.

Side note: generator is used to provide DI mechanism and static type checking (that the type system can tell you what dependencies are missing). Notably I didn't use react context. For hierarchy structure I mentioned this a little bit in my previous comments.

Thread Thread
peerreynders profile image
peerreynders

Interesting approach; I have to admit I didn't even know yield* was a thing.

that the type system can tell you what dependencies are missing

Yes but given IteratorResult<T, TReturn> my impression is that T is going to be union of the dependency types-so how would TypeScript check for the correct cardinality and ordinality of the dependencies? I also suspect that a minority of Developers are fluent with generators so that may present an additional cognitive barrier.

My personal experience has been that generators can be slow so it would be unfortunate if that would impose a premature performance ceiling; apart from the concern that generators are a runtime mechanism that may prove impossible to streamline with some design/compile time generated injection scripts.

But I'm sure I'm missing something.

One could argue "then everything is coupled with kairo"

  • Has it dependencies on a particular JavaScript runtime (e.g. browser vs. node; hopefully not)?
  • Does it have the potential to slow down microtests (> 100ms)?
  • Does it impede the ability to test relative small units of capability (hopefully not)?
  • It really boils down to how easy (and fast) it is to test (I'm not keen on the React/Jest situation).

Would const [count, setCount] = yield* State(initCount); actually work if it was run/tested independent from React? (seems UI would have to thread useState into App; one of my beefs with hooks it that they rely on a React specific mechanism).

Those are just my first impressions.

Thread Thread
3shain profile image
3Shain • Edited on

Yes but given IteratorResult my impression is that T is going to be union of the dependency types-so how would TypeScript check for the correct cardinality and ordinality of the dependencies?

Thant's why I'm using branded interface (nominal typing) so that I can 'add' or 'subtract' from a union of literal string (or object with literal string type field).

I also suspect that a minority of Developers are fluent with generators so that may present an additional cognitive barrier.

I thought it's not typical generator function but more like a DSL. And in theory it's closer to Algebraic Effects (or Effect Handling) so that generator only provides the capability to interrupt the control flow. Just borrowing the syntax, I'm not expecting (dev) users to fully understand generator (unless they want to hack into low level implementations).

My personal experience has been that generators can be slow so it would be unfortunate if that would impose a premature performance ceiling

Only one-shot execution. Much better than react's repetitive render call (and repetitive closure creation).

apart from the concern that generators are a runtime mechanism that may prove impossible to streamline with some design/compile time generated injection scripts.

they are likely different concerns. i'm not requiring devs to replace their every function with generator ones.

Has it dependencies on a particular JavaScript runtime (e.g. browser vs. node; hopefully not)?

purely ecmascript standard, purely runtime mechanisms.

Does it have the potential to slow down microtests (> 100ms)?

negative. if generator is thought to slow down executions, they only affect the construction process (which only happen once). I don't believe this will dramatically affect the performance.

Does it impede the ability to test relative small units of capability (hopefully not)?

not at all. it's one of the design goal.

It really boils down to how easy (and fast) it is to test (I'm not keen on the React/Jest situation).

it should be as easy as test against a regular class object (after all I'm utilising generator to implement an atypical "constructor" that handles DI and provide dependency type inference).

Would const [count, setCount] = yield* State(initCount); actually work if it was run/tested independent from React? (seems UI would have to thread useState into App; one of my beefs with hooks it that they rely on a React specific mechanism).

exactly. it's a stand-alone fine-grained reactivity impl (similar to solidjs)

it's the Component((_,$)=>...) API connecting two worlds. UI is a branded interface for React.FunctionComponent (from a top-down view, UI is the last (ultimate) concern (matches UI as an afterthought); from a down-top view, UI is the most concrete thing you can start with, and you can implement everything inside initially then move things out gradually). React is agnostic to anything else.


I'm think about a proper naming of this kind of framework because it's different from any kinds of existing ones. I had a idea kernel framework (from Functional Core Imperative Shell, that the UI frameworks frame the shell while kairo frames the core)

Collapse
peerreynders profile image
peerreynders • Edited on

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

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

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

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

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

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

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

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

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

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

The Real Cost of UI Components:

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

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

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

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

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

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

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

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

Collapse
redbar0n profile image
Magne Author • Edited on

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

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

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

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

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

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

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

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

Collapse
peerreynders profile image
peerreynders • Edited on

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

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

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

render(dropdown);
Enter fullscreen mode Exit fullscreen mode

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

Why are the components organized as a tree?

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

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

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

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

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

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


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

The HTML fragment (partial)

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

The CSS Block

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

  • structure
  • skin

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

The JS function, class, module etc.

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

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

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

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

Post-compile time there should be two separate artifacts:

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

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


When you express the sentiment that:

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

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

Thread Thread
redbar0n profile image
Magne Author • Edited on

Great insights, thanks for sharing!

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

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

You mention, very interestingly:

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

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

Thread Thread
peerreynders profile image
peerreynders • Edited on

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

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

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

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

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

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

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

Thread Thread
redbar0n profile image
Magne Author

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

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

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

Thread Thread
peerreynders profile image
peerreynders • Edited on

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

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

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

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

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

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

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

What is the glitch?

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

whereas it otherwise would be open by default?

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

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

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

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

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

Obligatory links to:

Thread Thread
redbar0n profile image
Magne Author • Edited on

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

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

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

Thread Thread
peerreynders profile image
peerreynders

Thanks.

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

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

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

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

The result:

Most components will be dumb

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

Also Complexity: Divide and Conquer! (2017).

Thread Thread
redbar0n profile image
Magne Author

Why are the components organized as a tree?

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

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

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

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

Thread Thread
peerreynders profile image
peerreynders

I presume you are already familiar with the article.

No I wasn't before now.

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

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

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

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

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

In my quote

Why are the components organized as a tree?

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

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

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

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


Earlier in this discussion I stated:

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

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

Thread Thread
redbar0n profile image
Magne Author • Edited on

I agree.

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

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

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

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

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

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

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

But I digress.

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

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

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

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

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

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

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

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

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

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

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

Thread Thread
peerreynders profile image
peerreynders

FYI:

The liabilities of MVC are as follow:

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

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

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

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

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

Thread Thread
peerreynders profile image
peerreynders • Edited on

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

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

My personal attitude

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

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

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

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

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

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

Components have become ViewControllers

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

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

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


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

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

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

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

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


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

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

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

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

Some of my pet peeves with Stimulus JS:

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

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

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

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

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

How would they compare?

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

Thread Thread
redbar0n profile image
Magne Author

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

Why change CSS at runtime at all?

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

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

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

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

Some of my pet peeves with Stimulus JS:

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

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

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

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

Thread Thread
peerreynders profile image
peerreynders

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

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

I wouldn't have thought of that myself.

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

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

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

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

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

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


Oliver Steele: The IDE Divide

That article differentiates between the

  • Language-Maven
  • Tool-Maven

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

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

Innovation Adoption Lifecycle

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


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

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

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

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

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

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

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

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

when it ought to have been a suffix, imho

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

Thread Thread
redbar0n profile image
Magne Author

I wish there was a non-UI core of Solid — for most purposes MobX seems a bit too heavy — I'd like something that is lighter than Redux.

Check out Daishi Kato's projects, Jotai, Zustand and Valtio. Zustand is the lighter but direct alternative to Redux (even though Redux-Toolkit, aka. RTK, has come far in approximating that DX). Easy-peasy is another alternative for simple DX, but sits atop of Redux. But Valtio might be what you are looking for, and even Kairo's 'atoms' seem interesting, for fine-grained reactivity. See: Jotai vs. Zustand vs. Valtio example code.

Also have a look at the new way of integrating MobX more cleanly / hassle-free with Solid: twitter.com/RyanCarniato/status/14...

Whatever you do keep actual page application logic out of the (Stimulus JS) controllers.

Where would you put it, then? How would you structure it?

It seems like this is what React achieves with their "hooks".

Thread Thread
peerreynders profile image
peerreynders • Edited on

Referencing this without comment just for your interest: How To Upgrade Your React UI Architecture

More interestingly Daishi Kato recently published When I Use Valtio and When I Use Jotai:

mirroring Mark Erikson's app-centric vs component-centric classification of React-based client side solution architectures.

(Also a custom hook for a data/app-centric design can just act as an adaptor/translation layer to an (otherwise self-contained) external dependency - simply delegating "business logic" rather than containing it.)

Edit: another one; React Architecture Confessions

Thread Thread
peerreynders profile image
peerreynders

Pete Heard's gone reactive now! (solid store anyone?).

Thread Thread
redbar0n profile image
Magne Author • Edited on

Thanks for the accurate reference! I’m sympathetic to the principle of separating out the state from the view. But at first glance this architecture seems over-engineered. He also doesn’t address the common concerns about 2-way data-binding (like uncontrolled cascading changes, and debuggability, esp. for large teams). It would be very interesting to hear @ryansolid’s opinion on this kind of reactive architecture, and how it compares with the likes of Recoil and solid-store. In particular, I’m not sure the ViewModel is really needed.

Thread Thread
peerreynders profile image
peerreynders

But at first glance this architecture seems over-engineered.

Hence my earlier comment


It would be very interesting to hear ryansolid’s opinion

Given his first comment I think the assessment of MVVM would carry over to this in particular:

"they tended to bloat and it created this second tier of indirection."

"they just recognized the separation of view and view model caused in many cases unnecessary complications."

"This isn't to say you can't have separate model data. "


Basically the components should subscribe to and report the client side domain model directly.

He also doesn’t address the common concerns about 2-way data-binding

I think the core concern is "data", not "2-way".

"2-way" shouldn't be a problem if a component reports user events (perhaps transformed/mapped) to the domain model without changing it's own core state (disregarding throttling etc.) and only changes its core state in response to events from the domain model.

The viewmodel almost seems like a layer to transform (domain model + events from domain model) into a pure view data representation.

To me it makes more sense to have the component poke the domain model directly (not by setting data, but with commands/events), and subscribe to the relevant events from the domain model while the component maintains its own view state.

With XState I came across the notion of extended state i.e. data that provides context for the current "state" but which can change without forcing a "state transition".

i.e. change of data != change in state

Perhaps the issue with the "M" in "MVC", "MVP(VM)", "MVI", "MVVM", etc. is that it is treated as raw data.

But as David Khourshid observes: "When do effects happen—State transitions. Always"

i.e. models can't just be a data dumping ground:

  • they must process inbound events to update internal data
  • they must emit outbound events only when internal data changes in a meaningful way (their state transitions).

In a way view related outbound events bind some of the controller responsibility to the model. To some degree this couples the "model" to the view the same way any service implements a contract to serve the needs of its clients. Then again the view is implementing a visual representation of the domain model on behalf of the domain model.

"Reactive data" is a step in the right direction but in some ways that is too fine grained unless that data only changes "on transition".

This is why I keep coming back to "The big idea is 'messaging'" (events, not components).

state + event = newState + actions [ref]

What are the "events" that will force the change of some part the UI's representation?

Thread Thread
peerreynders profile image
peerreynders

Are these components "just visual" enough?

Of course the "actual capabilities" exist here.

And as such the application behaviour can be tested without rendering or interacting with a surrogate DOM.

Thread Thread
redbar0n profile image
Magne Author • Edited on

As mentioned in the article, I dislike <Show> and <For> components, since they are basically control-flow logic, and don’t really represent a visual atom (“screen widget”, if you will) on the page. They are more Controllers than ViewControllers, imho. They strike me as more imperative than declarative, although I know Ryan disagrees on the interpretation of those terms, particularly in this context.

I have previously attempted to provide some suggestions to the SolidJS syntax for how I imagine it would look ideally (a “just JS” approach), see alternative 8.1 and compare with the equivalent “alternative 0” in SolidJS. But it wasn’t favored by the community.. There might be reasons unbeknownst to me for why it couldn’t have a syntax like that, given SolidJS internals…

That said, the separation of concerns you demonstrate with your solid-bookstore-a repo looks great. I think we ought to have more experiments of this sort!

It looks as if you effectively created a Model out of the SolidJS stores. The only thing I’m uncertain about is the intuitiveness of having to deal with createEffect and createMemo inside that model. Since those seem like rendering concerns... Ideally, I’d like to have rendering concerns wrapped inside some components (at least in the same file as the component), instead of turning the whole component model inside-out, like you’ve done here.

Would also have been interesting to see how it would look as a small state machine, using xstate/fsm xstate.js.org/docs/packages/xstate..., Robot thisrobot.life/ or as a Zag machine zagjs.com/ (from ChakraUI creator). Since the logic seems amenable to that.

Finally, I’m a bit wary against testing intermediate representations (as opposed to end-to-end, UI snapshots included). Since from experience bugs can always sneak in in the last layer towards the user. But yeah, for testing heavy logic, then isolating it like this presumably vasly improves the runtime. But it doesn’t make the rendering library (View layer) swappable.. (if that is a goal..). It’s probably the closest one can get to a View layer separation with current rendering libraries/trends.

Would it be possible to implement the same pattern with React? Or is it only possible because Solid’s fine grained reactivity?

Thread Thread
redbar0n profile image
Magne Author

But yeah, such terseness of the rendering result (without a bunch of inline ternaries etc.) is definitely something to strive for!

Thread Thread
peerreynders profile image
peerreynders • Edited on

They strike me as more imperative than declarative,

I think that's a matter of perspective based on your view of "declarative visual atoms".

HTML is semi-structured data. Putting aside that HTML is typically rendered (visual), the whole idea behind semantic HTML is that HTML has meaningful structure. You are declaring a structure that gives the data contained therein context.

Show is simply a meta structure that

  • is present when the data condition is satisfied
  • replaced with a fallback when the data condition is not satisfied or
  • missing altogether in the absence of a fallback

Similarly For/Index are meta structures that represent "repeating" data.

From that perspective Show/For/Index conceptually just extend HTML's capabilities of declaratively structuring data.


There might be reasons unbeknownst to me for why it couldn’t have a syntax like that,

My personal opinion is that my brain hates having to swap parsers mid stream when reading code. I'm either scanning for patterns in a declarative structure or I am reading flow of control (or transformation of values) code. Having to constantly switch mental parsers breaks the flow with repeated stalls.

given SolidJS internals …

The markup executes in a reactive context - it's conceptually a huge createEffect. So any Accessor<T> getters used in there will automatically re-subscribe on access (which is the entire point).

It's just simpler to avoid getting into JSX-style imperative code which can have an undesirable performance impact-just stick strictly to accessing data to be rendered or inspected and everything will be OK.


The only thing I’m uncertain about is the intuitiveness of having to deal with createEffect and createMemo inside that model.

It takes some getting used to. They're essentially read-only (but reactive) view models with command facades to trigger application changes (some facades also have queries for derived data or accessors for derived signals). One-way flow is preserved.


Would also have been interesting to see how it would look as a small state machine, using xstate/fsm

You quickly run into limitations with just finite state machines. In another experiment of mine I was surprised how quickly I needed to use compound state; i.e. upgrade to a state chart.


I’m a bit wary against testing intermediate representations

The intent is to include more logic in the fast feedback loop without slowing the tests down with rendering and comparing rendered output. The rendered output still needs to be tested with less frequently run integration tests.

The integration tests deal with the Horrible Outside World while the microtests exercise as much logic as possible without interacting with the HOW.

But it doesn’t make the rendering library (View layer) swappable.. (if that is a goal..).

It's not. Do you know of any cases with a web UI framework/state management situation where one was swapped while the other was kept? Typically both are generationally linked so when one is replaced the other is replaced with a generational match.


Would it be possible to implement the same pattern with React?

With just React?

I would imagine that would be extremely difficult if not impossible given that the component tree is at React's core. Hooks are specifically designed to operate on component instance specific state that is managed by React.

A component would basically use a single component specific hook to get everything it needs like data for rendering and input handlers. The hook would manage component state in as far it is necessary to re-render the component at appropriate times but would delegate everything else to the core client application that it can subscribe to via context.

But how do you exercise a hook without a component? A component is literally a render function for ReactNodes. You need to get the core client application out from under the render loop.

With Solid there is no render loop and the reactive state primitives are the core rather than some component tree. That makes it possible to build the application around state, independent from rendering. Solid's approach to rendering means that rendering concerns can easily attach to existing reactive state.

In React rendering and state are tightly coupled as component state primarily exists to inform re-renders rather than to enable application state.


A year ago I tinkered with a Preact+RTK version of the Book Store—I figured that the MobX version was perhaps not accessible enough for non-MobX developers (and I personally despise decorators; I find them arcane and unintuitive).

I didn't complete it but the main point was to avoid React Redux because Redux belongs inside the core client side application, not coupled directly to the visual components.

Also the core client side application should only expose methods for subscriptions and "commands" but not "queries". The "query data" would be delivered by the subscriptions.

function makeSubscription(cart, [current, setCurrent]) {
  // (1) Regular render data supplied by subscription here ...
  const listener = (current) => setCurrent(current);
  return cart.subscribe(listener, current);
}

function Cart() {
  const { cart } = useContext(Shop);
  // (2) ... but also accessed via a query method for initialization here
  const pair = useState(() => cart.current());
  useEffect(() => makeSubscription(cart, pair), []);

  const [ current ] = pair;
  if (!cart.hasLoaded(current)) return <Loading />;

  const checkout = () => cart.checkout();
  return renderCart(cart.summary(current), checkout);

}
Enter fullscreen mode Exit fullscreen mode

It's at this point I realized I needed to go back to the drawing board because the direct data accesses (queries) to the cart felt like a hack; all the necessary information for rendering should come in via subscriptions, not need to be accessed directly.

With Solid this was much simpler because components are setup functions; when the function runs we're clearly initializing.

With React function components

  • component initialization
  • initial render
  • subsequent renders

are all crammed into the same place. I needed subscriptions to the application to trigger renders so I was planning to send the necessary rendering data along. But for initial render I needed to get the data directly. So really the application data would be retrieved via direct data access (queries) anyway while the subscriptions only existed to "poke the component in the side" to start a render.

To move forward I would have to implement a separate query method for every type of the subscription the core client application would support. That just seemed all wrong.


"You need to initiate fetches before you render"

Ryan Florence: When To Fetch. Reactathon 2022

Corollary:

"Renders should be a reaction to change; not initiate change".

And for renders to be truly reactive you need either

  • an opportunity to create subscriptions before the first render or
  • have subscriptions that synchronously deliver data immediately upon subscription.

Neither is possible with React function components. Solid's subscriptions will synchronously deliver the (initial) data immediately upon subscription.

Thread Thread
peerreynders profile image
peerreynders • Edited on

I would have to implement a separate query method

Something along the lines of this:

// src/components/use-app.ts
import { useContext, useEffect, useState, useRef } from 'react';
import { getContext } from './island-context';

import type { App } from '../app/create-app';

type ContextRef = React.MutableRefObject<React.Context<App> | null>;

const getAppContext = (contextKey: string, ref: ContextRef) =>
  ref.current == undefined
    ? (ref.current = getContext(contextKey))
    : ref.current;

const advance = (current: number): number =>
  current < Number.MAX_SAFE_INTEGER ? current + 1 : 0;

function useApp(contextKey: string) {
  const ref: ContextRef = useRef(null);
  const app: App = useContext(getAppContext(contextKey, ref));
  const [, forceRender] = useState(0);

  useEffect(() => {
    return app.listen(() => {
      forceRender(advance);
    });
  }, [app]);

  return app;
}

export { useApp };
Enter fullscreen mode Exit fullscreen mode

i.e. useState() is only used to force a render…

// src/components/CounterUI.ts
import React from 'react';
import { useApp } from './use-app';

type CounterUIProps = {
  appContextKey: string;
};

function CounterUI({ appContextKey }: CounterUIProps): JSX.Element {
  const app = useApp(appContextKey);

  return (
    <div className="counter">
      <button onClick={app.decrement}>-</button>
      <pre>{app.count}</pre>
      <button onClick={app.increment}>+</button>
    </div>
  );
}

export { CounterUI as default };
Enter fullscreen mode Exit fullscreen mode

…while the app properties and methods are used to obtain the render values (ignoring the contents managed by useState() entirely).

Collapse
carloslfu profile image
Carlos Galarza

Great article! @redbar0n

Collapse
peerreynders profile image
peerreynders • Edited on

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

Redux maintainer Mark Erikson observed (2018):

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

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

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

React is a state management library

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

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

However Sebastian Markbåge points out:

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

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

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

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

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

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

Hypothesis: Hooks favour the "component-centric" view

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

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

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

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

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

Elsewhere he notes:

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

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

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

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

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

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

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

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

Patterns of Enterprise Application Architecture, p.161

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

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

Other References:

Collapse
redbar0n profile image
Magne Author • Edited on

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

I especially agree with:

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

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

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

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

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

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

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

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

As you said:

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

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

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

Collapse
brucou profile image
brucou • Edited on

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

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

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

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

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

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

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

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