DEV Community

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

 
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

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

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

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