DEV Community

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

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

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

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

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

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