JSX your way

20th March 2022

Produce and consume JSX in any way that you want

I have a vision to be able to write and read JSX trees in a way that is JavaScript flavoured

JSX trees have two kinds of information available about them.

The first kind is information that is readily available and accessible directly on the top referenced JSX node.
For example the name or type of the node, this is usually a string for known types, or a function for nodes that have resolvable information related to them.
Another example is the properties provided to the JSX node when it was defined.

In JavaScript, we can define "getter" functions that take an object, and returns the relevant value.

The thing is, different implementations of JSX use different object shapes and property names.

That's okay, we will just read every key and use the first one that matches!

This lead to the implementation of name, and properties

const { name, properties } = await import("@virtualstate/focus");

Give any object to these functions, and they will go through a list of known property names, and return the value

console.log(name({ name: "some name" }));
console.log(name({ tagName: "some name" }));
console.log(name({ source: "some name" }));
console.log(name({ type: "some name" }));
console.log(name({ [Symbol.for(":jsx/type")]: "some name" }));
console.log(properties({ properties: { key: "value" } }));
console.log(properties({ props: { key: "value" } }));
console.log(properties({ options: { key: "value" } }));
console.log(properties({ [Symbol.for(":jsx/properties")]: { key: "value" } }));
console.log(properties({ [Symbol.for(":jsx/props")]: { key: "value" } }));
console.log(properties({ [Symbol.for(":jsx/options")]: { key: "value" } }));

The above would log

some name
some name
some name
some name
some name
{ key: 'value' }
{ key: 'value' }
{ key: 'value' }
{ key: 'value' }
{ key: 'value' }

This opens up the pattern of allowing some intermediate function providing a layer between our defined JSX objects, and

The second kind is information that is not readily available, and requires resolution.
For example children that resolve their state, either asynchronously or not.

Following the same pattern used for name we can define a children function that gives us the same kind of access.

const { children } = await import("@virtualstate/focus");
async function Wait({ tasks }, input) {
  while (tasks -= 1) {
    await new Promise(queueMicrotask);
  }
  return input;
}
const node = (
  <parent>
    <Wait tasks={10}>10</Wait>
    <Wait tasks={20}>20</Wait>
    <Wait tasks={30}>30</Wait>
  </parent>
);
console.log(await children(node));

The above would log

[10, 20, 30]

In this case it could take any number of steps to complete resolution. Using an async thing  we can provide a unified object that can be used with both await and for await, providing a way for consuming code to look at children as a whole, or seeing the progression of resolution through each set of updates.

for await (const snapshot of children(node)) {
    console.log(snapshot);
}

The above would log

[10]
[10, 20]
[10, 20, 30]

Using for await, without knowing whats happening internal to the JSX node, we can iterate each time we receive a snapshot of the known children state, with the final iteration being equivalent to the original await based code.

Given that we have a way to watch multiple updates for the same child, meaning children can be async generator functions.

async function *Tasks({ tasks }, input) {
    yield "Starting";
    while (tasks -= 1) {
        await task();
        yield `Remaining tasks ${tasks}`;
    }
    yield input;
}

const node = (
    <parent>
        <Tasks tasks={3}>Done</Tasks>
    </parent>
);

for await (const snapshot of children(node)) {
    console.log(snapshot);
}

The above would log

["Starting"]
["Remaining tasks 2"]
["Remaining tasks 1"]
["Done"]

Multiple children can produce multiple states at the same time, if two children resolve within the same microtask, you most likely will see their results in the same iteration. Either way, going back to just using await shows the complete resolution:

const node = (
    <parent>
        <Tasks tasks={9}>Done 1</Tasks>
        <Tasks tasks={1}>Done 2</Tasks>
        <Tasks tasks={7}>Done 3</Tasks>
    </parent>
);

console.log(await children(node));

Would log

["Done 1", "Done 2", "Done 3"]

Given that each JSX node can optionally have resolvable children, a tree of descendants can be resolved by stepping through each child (and so on) to produce a full representation of the entire available state. This is another example of the second kind of information that we want to read from a JSX tree.

The resulting repository & module used in this post can be found at github.com/virtualstate/focus