Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active August 31, 2023 09:52
Show Gist options
  • Save WebReflection/9eca0aa749c6f06ce19159a1bd58dc4f to your computer and use it in GitHub Desktop.
Save WebReflection/9eca0aa749c6f06ce19159a1bd58dc4f to your computer and use it in GitHub Desktop.
Brainstorming a smarter JSX

Brainstorming a smarter JSX

Deprecated

The new proposal and hopefully the new transformer will be out soon.


This gist tries to narrow down all features and complexity hidden behind a smarter JSX, something landed already as @ungap/babel-plugin-transform-hinted-jsx module but not yet concretely, or efficiently, implemented.

JSX Containers

  • ✔ means definitively
  • ❌ means absolutely not
  • ✅ means more or less
Element Fragment Component
can be outer most template
can be parsed/traversed AOT
can be a static child
can be an interpolated child
can be mapped as DOM tree
can return a different tree
can receive props
will use props for attribute
can have in scope effects
can be updated via effects

Element

<div some={"dynamic"} attr="static">
  Child as string or <span>element</span>
  <Component {...props} />
  <>
    Useless
    <span test={"case"}>Fragment</span>
  </>
  {<a /> || <><b /></> || <Component /> || 'primitive' || [...nodes]}
</div>

When used as outer most JSX template it can be parsed AOT and produce the following content:

<div attr="static">
  Child as string or <span>element</span>
  <!--Component-->
  Useless
  <span>Fragment</span>
  <!--interpolation-->
</div>

This structure can be also mapped through a list of operations to perform atomically per each targeted node, either as attribute to update/add/remove, or inner content to also update dynamically.

The mapping can be done in a way that a single pass with invokes per each update would do the right thing, where the right thing is likely:

  • add, remove, or update the some attribute with the interpolated value at the top most root node
  • grab the pin-pointed component and inject its outcome either if it's the first render, or if its result is different from the previous one
  • add, remove, or update the test attribute with the interpolated value at <span> element, as child node 3 (map it before injecting components or interpolations)
  • update, if needed, the interpolation child node 4
Pleasae note

The amount of arguments to crawl to do such mapping might be different from the child nodes to reach, specially in the static fragment case, as it disappears from the DOM. This is not really a common use case but, being valid JSX, we need to keep that difference into account.

Comments

The usage of comments within a template helps pin-pointing those parts that might change in the future. While a Component that returns always the same element could replace such node with its content, and if its content is not a fragment it's possible to replace the element in the future with conditional elements or components too, for simplicity sake we could just keep the comment and work through insertBefore keeping track of possible fragments or node previously returned.

The same applies for interpolations: these can return at any time anything that is not uniquely pin-pointed on the element tree.

Tracking different content

Either interpolations or Components could return different content after effects or conditions and the way the transformer allows checking if the current content is the same as before is through the private __token props field, unique per each outer most JSX template. The rest of the static content doesn't need diffing or tracking.

Diffing different content

I am trying to avoid any need for vDOM indirection but a smart diffing utility should be used when content is replaced. When it comes to interpolations or Components this is not really relevant or useful, as these will have, and produce, different nodes anyway, but if an interpolation contains as Array, it's likely to be returning the same list of nodes that needs diffing, either by keys or by position (e.g. a classic TODO list of items).

Fragment

<>
  Static child
  <Component {...props} />
  <static attr="element" />
  {<a /> || <><b /></> || <Component /> || 'primitive' || [...nodes]}
</>

A fragment can either be static or dynamic. When static, it's basically a useless, needless, placeholder that can disappear from any DOM parsing and manipulation, delegating updates to its outer top most, element. When it's dynamic though, as example returned by another component or as part of an interpolation, operations involving its content should be mapped related to the fragment container. In that context, fragments are pretty much like elements with their own list of updates per each node within their content.

Component

function Component(props, ...children) {
  const {attr, somethingElse} = props;
  if (somethingElse)
    setTimeout(somethingElse);
  return <a /> || <><b /></> || <Other attr={attr} />;
}

A component can conditionally return an Element, a Fragment, or another Component, and it mustbe handled as effect so that props, or even children, passed as signals, would trigger the same component with the latest received arguments, updating all parts that changed in the meantime.

The main difference with elements and fragments, is that components cannot really be parsed ATO unless invoked, so it makes no sense to map them as <Component /> before these get revealed.

Because components can have logic related to the passed properties, these should not be passed as interpolations, hence there shold be a mechanism to de-interpolate values before their execution, and a way to understand if the returned content is the same as before, still through the __token, or it's new content that needs to be fully replaced.

For simplicity sake, components can keep their pin-pointed reference on the DOM through their related comment, but some smarter logic could keep track of last inserted node or fragment, and replace these with the new content, as long as it's being tracked for the future too.

TODOs

  • how to treath key within Arrays (and ignore these for non list related parts)
  • how to populate ref out of the box after first execution/render
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment