DEV Community 👩‍💻👨‍💻

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

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

 
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.