DEV Community

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

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