DEV Community

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

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)