Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How could JSX work natively? #5

Closed
thesoftwarephilosopher opened this issue Aug 18, 2024 · 36 comments
Closed

How could JSX work natively? #5

thesoftwarephilosopher opened this issue Aug 18, 2024 · 36 comments

Comments

@thesoftwarephilosopher
Copy link
Owner

One goal of this project has been to experiment with what JSX could possibly look like in the future if it ever became native syntax. What would the formal spec say about its implementation? How could it be as isolated and decoupled from every framework as possible, while still being convenient?

I'm opening this issue for discussion/brainstorming on this topic. We could use experimental versions of imlib as a testing ground for it.

@thesoftwarephilosopher
Copy link
Owner Author

thesoftwarephilosopher commented Aug 18, 2024

What I don't like about existing JSX implementations is that they require a bundler.

What I don't like about imlib's current JSX implementation is that it requires a runtime (since it's not bundled).

Ideally, JSX would compile to something native:

Maybe JSON trees?

<a href="foo">bar</a> === { a: [ { href: "foo" }, "bar"] }

I like this because it's completely agnostic, and leaves parsing to the user.

I don't like this because it's not as convenient:

div.replaceChildren(<b>test</b>) // no longer possible
div.replaceChildren(toDom(<b>test</b>)) // have to transform it with a a helper now

Maybe well-known class preexisting at runtime?

<a href="foo">bar</a> === new JSX({ a: [ { href: "foo" }, "bar"] })
// or just
<a href="foo">bar</a> === new JSX(a, { href: "foo" }, ["bar"])

This has the same flaws as React.createElement: you have to import this object into the global namespace somehow.

If JSX is ever made native, this wouldn't be an issue. A JSX object could be provided by the browser, and could have a [[toDOM]] symbol-method, that DOM methods implicitly invoke when present. This method could also be useful for frameworks.

But if JSX isn't going to be a user-defined class, what's the point of JSX syntax providing this instead of DOM nodes?

@haggen
Copy link

haggen commented Aug 19, 2024

I don't know if this is the place but;

  1. Awesome stuff, I've been thinking about JSX in a frameworkless environment for the longest time. I think deno has something similar.
  2. In the Todo example you render {list.ul} which is good, but it made me think it could be better if we expected a toJSX() on the variable. This way we could tell how the value is represented as JSX, just like we do with toString() and toJSON(). list.ul reads dirty because we're referencing internals of the class.

@thesoftwarephilosopher
Copy link
Owner Author

@haggen I get where you're coming from, because typically in React-style JSX, we expect everything to be JSX syntax. In my examples for vanillajsx.com, I tried to write it in the simplest possible way, getting rid of the assumption that everything needs to look like React. In this case, a TodoList object simply has a ul: HTMLUListElement, which you can then embed. I see that it looks inconsistent, and technically it's using inconsistent syntax. But I think semantically it's very clean, and I don't see a reason to add any complexity to the semantics to unify the syntax, for the same reason that I think Lisps look very ugly semantically but syntactically very clean, and 5 years of writing Clojure professionally was more than enough to convince me the simplicity of the syntax isn't inherently better, which is why I turned to writing TypeScript full time, which has a much more complicated syntax than Clojure, but which matches real-life complexity, and therefore none of the syntax is unnecessary complexity that should be simplified.

@haggen
Copy link

haggen commented Aug 19, 2024

@sdegutis Makes sense. list.ul still feels like accessing class internals but we can always call our representation method explicitly, e.g. <div>{list.toJSX()}</div>.

@thesoftwarephilosopher
Copy link
Owner Author

@haggen I think it makes sense for DOM methods, like div.append or span.replaceChildren to coerce methods into DOM if a well-known symbol method exists on them, like [[toDOM]], similarly to how for..of looks up Symbol.iterator on your object. In my opinion this is a good starting point for making JSX generic, since it's orthogonal to JSX, but can work well with it. I wonder if these DOM methods can be shimmed to do this now?

@thesoftwarephilosopher
Copy link
Owner Author

The challenge of Vanilla JSX is that there's no runtime. One has to be looked up or injected at compiletime or JS runtime.

Even in the proposal above, [[toDOM]] has to be an object on something. If we inject a class at runtime, it's no different than auto-importing jsx by the compiler.

I think this is what everyone is stuck on in terms of even coming up with a way JSX could become native.

@thesoftwarephilosopher
Copy link
Owner Author

This is why I like the JSON-tree solution above:

<a href="foo">bar</a> === { a: [ { href: "foo" }, "bar"] }

Then frameworks are all free to do whatever they want with this tree.

The difficulty for vanilla jsx is that it's not as convenient. You can't just add that into a DOM without converting the tree first.

The difficulty for React is (as far as I remember from 2014) that it's slow. I don't know if this is still true today though.

@phaux
Copy link

phaux commented Aug 22, 2024

@thesoftwarephilosopher
Copy link
Owner Author

@phaux thanks, it's a clever idea, and more use-cases that show what vanilla JSX could be like, and what baseline it would need to satisfy.

@thesoftwarephilosopher
Copy link
Owner Author

I had the idea yesterday to embed the JSX tag, attributes, and children, all into the same object literal:

type JSX<Tag = any, Attrs extends { [attr: string]: any }> = { jsx: Tag, children: any } & Attrs;

<a href="foo">bar</a> as { jsx: "a", href: "foo", children: ["bar"] }
<Foo x='1' y='0' />   as { jsx: Foo, x: '1', y: '0' }
<hr/>                 as { jsx: "hr" }

I think it's clean, semantic, and twice as memory-efficient as the previous idea.

The downsides are:

  • You can't have jsx as an attr name.
  • Existing objects with this signature (unlikely but possible) will look like valid JSX to consumers of JSX, especially {jsx:'b'}

@thesoftwarephilosopher
Copy link
Owner Author

I wonder...

What if there becomes a built-in JSX class (shimmed at first), which is entirely configurable by libraries?

The first way I can think of is that libraries could set its base class (prototype) at runtime. Two problems with this:

  1. It would have to happen after initialization, and I'm guessing most factory functions do a lot of initialization
  2. It would be a global change at runtime, so that only one library can replace it at a time

@thesoftwarephilosopher
Copy link
Owner Author

The plain JS object syntax is available for testing in 3.0.0-beta1

@nhh
Copy link

nhh commented Aug 29, 2024

Hey there, just skipped through. Love to see other people falling in love with jsx and plain DOM. I am working on a maybe-similar thing. https://github.com/nhh/zero

You might want to check out my jsx dom type mappings. https://github.com/nhh/zero/blob/main/zero.ts

I already started building stuff with it. https://www.0xfff.de/zero/index.html

Its nowhere near finished or polished, I just started exploring TSX last week. (Inspired by vanillajsx.com)

@thesoftwarephilosopher
Copy link
Owner Author

I just published a proposal to standardize JSX.

Would appreciate any feedback.

@cowboyd
Copy link

cowboyd commented Sep 2, 2024

Let me start by saying that this is so incredibly simple, that it makes me want to cry.

As for feedback: The fact that children is in the attribute namespace does not seem desirable apart from the fact that there is a historical context within "classic" JSX transform for it to be so.

For example:

const b4 = <web-component children="xyz">{"Hello"}</web-component>

compiles to:

const b4 = {
  [Symbol.for("jsx")]: "web-component",
  children: "xyz",
  children: ["Hello"]
};

Suppose that web-component was implemented with compiled WASM and used the children attribute for its own purposes. We would not be able to represent it cleanly in JavaScript.

Rather than use a Symbol.jsx, why not just use simple names for a simple interface?

{tagname: Button, attributes: { children: "xyz" }, children: ["Hello"] }

It's concise, unambiguous, and very readable.

I understand why one would want to have the attributes at the highest level to ease access, but at the same time, I think most JSX literal value usage will be very "meta" by its nature. I.e. read and managed by library code, not directly by user code, and so I don't think having the attributes mixed into the top level object will actually improve the experience.

@cowboyd
Copy link

cowboyd commented Sep 2, 2024

If you want to "know" that something was a JSX, then I would have a JSX constructor that you can use to check with instanceof a-la existing literal constructs

{} instanceof Object //=> true
[] instanceof Array //=> true
/xzy/ instanceof RegExp //=> true
<div/> instanceof JSX //=> true

@thesoftwarephilosopher
Copy link
Owner Author

@cowboyd I like that idea, it seems clean, fits the pattern, and is very easy to implement when using the classic react transformation (pragma: 'new JSX', pragmaFrag: '""',). My only hesitation is that there might already be classes called JSX elsewhere. Using automatic runtime makes it slightly less efficient, since there will have to be a jsx function that just calls new JSX, but at least there's no global. But the ideal toward standardization would be making JSX class a global as part of the spec, and transforming it in the classic way above.

@thesoftwarephilosopher
Copy link
Owner Author

@cowboyd Although I like the consistency of that method, I'm not sure it would be good on the path to standardization, namely because of the need of a class that would need to either be imported or become a global. I feel like those two options would impede standardization, whereas the same semantic check is possible with the symbol solution that I proposed above. Plus, the class would literally be functionality-less, as I can't imagine any common functionality for a JSX expression between all frameworks, except for the semantic check itself.

@thesoftwarephilosopher
Copy link
Owner Author

I've updated my JSX standardization proposal to fix efficiency issues brought up elsewhere.

At this point I'm convinced this is the only form of JSX transformation that has a chance to be standardized into ECMAScript, because it's self-contained, simple, and roughly as efficient.

For those reasons, I'm going to move forward with this implementation in all my projects.

But I also doubt the JS community has any interest in moving JSX forward in any direction other than what's dictated by a few primary entities, and I have no ability to spread interest in this proposal. So I'm done trying to advocate for it.

@cowboyd
Copy link

cowboyd commented Sep 4, 2024

feel like those two options would impede standardization, whereas the same semantic check is possible with the symbol solution that I proposed above.

I feel the opposite is true. This would be a novel use of Symbol which in all other instances is used to represent an interface or a protocol between an object and the javascript runtime, not a marker for developers. If anything, a Symbol.jsx would imply to me a method to convert any object into jsx. As such, it would be a barrier to adoption.

But this is a quibble, and one that can ultimately be adjudicated by feedback on a proposal. However, I think that the basic technique is so radically simple, and solves so many problems that I think it actually is possible that it gains traction and makes it through the standardization process. It would no doubt be a lot of work, but I think the biggest objections would come around two things: 1) will it work with React, and how will it perform in comparison. If the answer is "well", and "well", then it would make any argument against this proposal pretty thin. Maybe there is a good reason why this isn't a good idea, but I would love to find out why.

@thesoftwarephilosopher
Copy link
Owner Author

@cowboyd (a) that's just the typical use of Symbols; inherently they have no purpose aside from being unique strings, and that solution fits well here; (b) for it to gain traction, it needs to be heard, and I can't effectively get anyone besides you (and one preact dev in deno who seems strongly biased against it) to hear my proposal; (c) I highly doubt it could ever be as fast/efficient as rendering JSX as a function call that's literally designed/tailored for React.

@nhh
Copy link

nhh commented Sep 4, 2024

You are heared. Just dont give up so early. You got this 👍

@thesoftwarephilosopher
Copy link
Owner Author

Even though it literally has no chance to be as efficient/fast as the status quo, everyone knows that React is already slow, and if people see the advantage of JSX being syntactic sugar for plain objects, they might be okay with the slight performance loss. But React is the king of JSX mindshare, and everyone loves it, and I highly doubt anyone will care about what JSX can do outside of it; except other framework authors, who are the only ones capable of pushing this proposal. And I have been slowly creating utility libraries around vanilla DOM to wrap common patterns, which I guess could gain traction if I push further into that and clean it up, especially if it shows the true convenience of vanilla JSX. The last todolist example on https://vanillajsx.com/ has a lot of low hanging fruit for extraction to a lib, esp. around clean handling of events in DOM trees respecting node-moving.

@nhh
Copy link

nhh commented Sep 4, 2024

I disagree. I am already working on a helm alternative in jsx which is a wildly different usecase. https://github.com/nhh/k8x

People will get it too

JSX is awesome and does not only belong to react or the browser.

@thesoftwarephilosopher
Copy link
Owner Author

@nhh that's actually pretty interesting, but why not just use JSON? That's already standard for config use-cases like this, no?

@nhh
Copy link

nhh commented Sep 4, 2024

Compare json to jsx and you got your answer 😁

Edit: I worked with helm for several month now. Our charts are full of variables, helpers, hard to read and im general extremely hard to configure and reuse. k8x aims to solve that.

@thesoftwarephilosopher
Copy link
Owner Author

I could see updating https://vanillajsx.com/ to add a comprehensive explanation, use-case examples, a full Q&A, and make it like a full page thorough argument in favor of vanilla JSX, if others were willing to post it to various forums to see if it can gain traction.

Other than that, I'm out of steam for this cause. My only task left is publishing @imlib/babel-transform-vanillajsx-plugin.

@nhh
Copy link

nhh commented Sep 4, 2024

Maybe its ok to get some distance to the whole standardization thing. Just experimenting with what we already have and appreciate where it can go. It does not need to be standardized to be a good idea. Bundler wont go anywhere in the next decade, so a transpilation step is also not that big of a deal.

@thesoftwarephilosopher
Copy link
Owner Author

@nhh my goal is to make writing immaculatalibrary.com as smooth and easy as possible, and that's where my vanilla JSX came from; types are coming to vanilla JS, the only major thing left is JSX, and I would love to get rid of a transpiler entirely. That's been my entire motivation for vanillajsx.com and this proposal for standardization. But in retrospect, yeah, it would be removing only about 20 lines of code from imlib, and only when both types and JSX become native. So good point, I agree. (Ideally, I don't want to have to maintain any lib at all, I want everything to be native; but imlib has several other efficiency/performance features I can't find or easily build on top of anything else right now unfortunately; without it, maintaining immaculatalibrary.com would be orders of magnitude slower.)

@cowboyd
Copy link

cowboyd commented Sep 4, 2024

What got me interested in this in the first place is my work with MDX which is a massive use-case with a large community. And there, the ability to not be able to transform JSX in different ways within the same file is a real millstone.

But in any case, thanks for the time you put into this.

@nhh
Copy link

nhh commented Sep 4, 2024

And there, the ability to not be able to transform JSX in different ways within the same file is a real millstone.

Just an idea: Write custom loader/plugins for vite and return different jsx factory invocations.

import {Chart} from './snowfall.js?customLoader'
export const year = 2023

# Last year’s snowfall

In {year}, the snowfall was above average.
It was followed by a warm spring which caused
flood conditions in many of the nearby rivers.

<Chart year={year} color="#fcb32c" />

@thesoftwarephilosopher
Copy link
Owner Author

Just an idea: Write custom loader/plugins for vite and return different jsx factory invocations.

Then the ?customLoader would bread IDE support until VS Code et al special-case that too.

Ugh, the JS ecosystem is so fragmented, disorganized, ad hoc and messy. I sometimes feel like Sauron and just want to swoop down and take control and fix everything for everyone. But the countless use-cases are so complex that I know none of us probably can fix this mess, individually or collectively.

@nhh
Copy link

nhh commented Sep 5, 2024

Getting back to the original question, maybe it would be useful to layout what is needed to have a proper implementation roadmap. Something like

  1. Define jsx syntax standard
  2. Define how it's get parsed and transpiled, provide a reference implementation. (maybe use esbuild/babel as reference)
  3. Define what is needed to support that in all major browsers, parsing engines, devtools, js runtimes. (thats huge)
  4. Define how the transpiled factory methods look like
  5. Define who is gonna provide the factory functions

With that we can get a somewhat realistic feeling for the impact that this standardization has, and maybe better understand why everybody is just fine with using a preprocessor/buildtool for this.

Personally, I think relying on a preprocessor like tsc, esbuild or babel is fine, because using typescript will always need a build chain and it already supports typing and jsx-factory configuring.

What do you think?

@thesoftwarephilosopher
Copy link
Owner Author

@nhh I've settled on this JSX transformation as the most reasonable path to standardization. The babel plugin source is linked on that page. Runtime authors said they won't adopt it until framework authors adopt it en masse. Framework authors probably won't adopt it because it's inherently (slightly) less efficient and less ✨magical✨ than the status quo:

// now

<a href='foo'>bar</a>
// transforms to
import _jsx_ from 'react/jsx-runtime';
_jsx('a', {href:'foo'}, 'bar')

// with this proposal

React.createElement(<a href='foo'>bar</a>)
// transforms to
React.createElement({ [Symbol.jsx]:true, tag:'a', attrs:{href:'foo', children:'bar'} })

I promise you nobody will prefer the second except people like me who can't stand React who are in the vast minority. Then again, React 19 has new 'use client' etc directives to put at the top of files, so maybe it won't be long until more people see how stupid this framework is. But I'm convinced most software engineers don't think for themselves anymore.

@nhh
Copy link

nhh commented Sep 5, 2024

Thx for inspiring me with vanillajsx! Since reading about it, I habe a ton of fun abusing jsx syntax to my will 😄😁😁

@thesoftwarephilosopher
Copy link
Owner Author

thesoftwarephilosopher commented Sep 5, 2024

Also I do see opportunities for the desugared object to be smaller or more efficient or whatever. For example

// given
const b1 = <a href='/foo'>Click me</a>;

// instead of 
const b1 = {
  [Symbol.for("jsx")]: true,
  tag: "a",
  attrs: {
    href: "/foo",
    children: "Click me"
  }
};

// it could become this which mimics jsx() parameters
const b1 = {
  [Symbol.for("jsx")]: ["a", {
    href: "/foo",
    children: "Click me"
  }],
};

// or this which mimics React.createElement() parameters
const b1 = {
  [Symbol.for("jsx")]: [
    "a",
    {
      href: "/foo"
    },
    "Click me"
  ],
};

But (a) anyone who has sway in standardizing it would probably prefer the more explicit form, and (b) people like this guy would probably consider it less compatible with JS engine optimizations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants