Returning actual DOM nodes entirely blunts the big advantage of JSX (and non-JSX libraries like Lit) - which is their immediate mode style API, and UI=f(state) model.
You want to return a description of the DOM, rather than the real DOM, because you want to be able to reevaluate your templates repeatedly with new state, and efficiently update the DOM where that template is rendered to.
All the examples here use imperative DOM APIs to do updates, like with this:
function TodoInput(attrs: { add: (v: string) => void }) {
const input = <input /> as HTMLInputElement;
input.placeholder = 'Add todo item...';
input.onkeydown = (e) => {
if (e.key === 'Enter') {
attrs.add(input.value);
input.value = '';
}
};
return input;
}
class TodoList {
ul = <ul class='todolist' /> as HTMLUListElement;
add(v: string) {
const item = <li>{v}</li> as HTMLLIElement;
item.onclick = () => item.remove();
this.ul.append(item);
}
}
Avoiding those `input.onkeydown = ...` and `this.ul.append(item)` cases, and instead just iterating over items in your template, is probably the main benefit of a VDOM.(The problem with VDOMs is that diffing is slow, a problem solved by using templates that separate static from dynamic parts, like Lit - a library I work on).
From my experience creating complex web UIs, the performance angle of using a vdom is pure fantasy if your application is complex enough.
In fact I now strongly believe it's counter productive, because most people come to it thinking "I can just trigger however many re-renders of this large piece of UI as I like, the vdom makes it ok" and it doesn't, the performance sucks, but now you have architected the app in a way that requires a rewrite to make the app perform well.
I have seen this exact sequence of events four times, by four different teams. The second, third and fourth, as a principal architect consulting for the team I tried to intervene and advocate for a vanilla architecture that is mindful about performance, citing the issues they would likely experience with react, but to no avail. There was a lot of "oh but there many ways to avoid those issues" followed by a list of things I was presumably ignorant about.
I guess most of us need to learn things the hard way.
There were two groups I was hoping vanillajsx would resonate with. The first is people who still buy into the React dream but are beginning to be disillusioned with its inability to deliver on its promises, and the second is people who already are fully disillusioned.
Specifically, I'm hoping to show that vanilla architectures can be not only performant, but easy to maintain with well designed code that uses stable and known patterns. Using JSX just so happens to clean up the code nicely and make the relationship between React and vanilla very visible, but that's really all it did here.
Although to be fair, the hack required to get JSX-as-DOM to work is really unfortunately and I'm not very happy with it, and I would prefer JSX to just render as an object tree that anyone can render however they want. But when I tried that for a few months or a year, it was not nearly as performant as rendering them as strings as soon as they're evaluated, which can then be cached via standard module caching. At least, that's how I got immaculatalibrary to entirely render all HTML files in ~700ms initially and ~70ms on most file changes.
I'll try to do some experimentation next week to see if I can get more performance back out of having <foo bar={qux}>child</foo> to render as {foo:{bar:qux, children:[child]}} again though, because that would absolutely be the ideal, and would unfork JSX in the same way Typed Annotations proposes to unfork JavaScript types.
Thank you for posting this! VanillaJSX is refreshingly different, and we desperately need new ideas in the front-end space to reduce the complexity and get closer to the browser. I also feel like the discussion in this thread is very rich and gives people on both sides of the fence a lot of stuff to think about.
There were two groups I was hoping vanillajsx would resonate with. The first is people who still buy into the React dream but are beginning to be disillusioned with its inability to deliver on its promises, and the second is people who already are fully disillusioned.
I don't know if you've seen it, but Alex Russell just did a blog series where he directly talks about this disillusion and proposes a move away from React for most web apps: https://infrequently.org/series/reckoning/
I am not as anti-React as that myself, but I do agree it is hard to scale up and have it perform well, not at all like the promise. As always, there are no silver bullets and you have to pick a stack that you can understand.
By the way, I made my own pitch for fully vanilla web development here: https://plainvanillaweb.com/
IMO that blog series misses the point. Knowledgeable motivated developers can make great experiences with any technology, and conversely there are bad experiences built with every technology. That series blames the people involved for not being better, but that’s just blaming plane crashes on human error and calling it a day.
- If the UK GSD is anything like USDS, using them for comparison is like comparing a pro sports team to your local high school’s. They are an outlier specifically created to be better than the average, so tautologically their stuff will be better. Code For America is a similarly odd comparison.
- The US has a massive gap in pay and prestige between public and private sector developer jobs. It’s not that this means “worse” people work at public jobs, but in general they start less experienced and can wind up in a non-learning cycle as they don’t get mentorship/guidance from more expert folks, and if they do get good independently they leave. It’s really hard to convince people to take a pay cut to work these jobs, and many of the few willing to do so instead go to CFA, USDS, etc because they want prestige and avoid all the other inefficiencies in public jobs.
I could go on about the structural problems leading to this, but suffice it to say that blaming React and other JS frameworks is a miss. For some services it’s lucky they are online at all, and a slow web page is still orders of magnitude faster than physical mail or god forbid going to a physical office. The sites could definitely be better but this is not fundamentally a problem of technology choice.
You might find this project[0] interesting if you haven’t given it a look.
It was attempting to do something along the same lines as you first suggest
[0]: https://github.com/jridgewell/jsx2
Yes and the solution is to put on your big boy pants and to actually do your front-end application architecture properly.
Separate source of truth from derived data. Separate possibly valid user intent from validated state. Use contexts to organize the data dependency graph of your application. Ensure all your widgets use a consistent value type in and out, don't let events contaminate it. Use something like cursors or optics to simplify mutations and derive setters automatically.
I've never had an easier time building very complex UI functionality than with React. But it requires you to actively start reasoning about change in your code (what doesn't change), and this is something most people are not used to.
Personally I think React compiler is folly for this reason: they are taking the most interesting part of React, the part that lets you write apps that are incremental from top to bottom, and telling you it's too complicated for you to think about. Nonsense.
The issue is just that React makes pros feel like idiots unless they eat some humble pie and grok the principles and the reasons behind it. Which is that React is what you get when you try to come up with a UI architecture that can make entire classes of problems go away.
Without a VDOM, one way data flow, and diffing, your UI won't just be slow, it'll be full of secret O(n^2) cascades, random ifs to stop infinite cycles, random "let's update this state early so other code can immediately use it" ordering issues, and so on.
How many frameworks did you have experience with?
you wanted to write "or" not "and", didn't you?
you can adhere to the same principles (one way data flow) without vdom. Not saying it's easy at large scale but it's possible. I don't appreciate people invoking fud towards anyone opting out of their tech choice.
And from my experience building complex web UIs, those team members were right -- there are many ways to avoid the issues and using vdom is great in general. True, there are situations where it falls short, which is why you will want to fall back to other techniques for those bits of architecture. Just like your JS, Python or Ruby server will call a bunch of functions written in C or the like. That doesn't mean you should write your entire backend in C.
Yes, there are ways to avoid the issues, and they involve abandoning the immediate mode illusion that react created in the name of simplicity.
Write it in React, and if you run into performance issues, there are a bunch of well-known performance optimizations you can make which are easy to discover. It's a well-trodden path that many engineers have walked before.
Write it in your own vanilla framework, and you will effectively re-invent all the complexity of React, but in a way that no one has ever done before. It's easy at small application scales, but once your app gets large, good luck debugging the thing that exists primarily in your principal engineer's head.
Wasn't the issue mostly solved with signals?
As far as I understand, signals make it much easier to keep the DOM updates to a minimum.
It sounds to me like GP got told stuff exactly like this, with the team eventually not actually doing the thing.
More than once I got asked on interviews why react is faster than vanilla JS and I had to tell them no, it isn't.
The clue would be in the fact that react is running in vanilla JS.
There is a persistent ‘learned helplessness’ tendency among some developers to assume that the frameworks they are using have access to magical mystical powers above and beyond those that their own code can make use of.
Framework code might well be better optimized or more tuned than the code you would write - but if you cared to employ similar techniques you could achieve those same gains; on the other hand, since by definition it’s more flexible than single-purpose code, it might not be optimal for your usecase.
you shouldn't project from react to vdom in general:
https://dioxuslabs.com/blog/templates-diffing
The "performance angle" isn't really an angle. It gets bandied around by junior devs new to React, but it's not the primary selling point of React - in fact, it's not a selling point at all. Don't believe me? Just go to http://react.dev and look - no where on the site does it say that React is a hyper-performant library. It's not! If you need blazing performance, you're best off using something much more minimal, or even vanilla JS.
When people say that React is fast, what they mean is that React can dom-diff faster than a naive O(n) approach. It means that updating a component with a thousand nested divs won't crash out your browser, like it might if you were to write the code by hand. It doesn't mean it's an objectively high-performing framework.
What React is good at is forcing you to write code in a clear, comprehensible way. Having every engineer on your team obey F(props) = state is a strict improvement over virtually any other paradigm. (Yes, you can still make a tangle of components if you try hard enough, but the complexity of the tangle is capped significantly lower than the complexity of a tangle of JS without any framework attached.)
Depends who "you" are. I prefer to have my DOM nodes updated in place without all the reconciliation machinery. (no implication about what you want)
You don't need diffing or reconciliation to turn a description of DOM into DOM. Lit works without a VDOM.
If all JSX does is return a DocumentFragment that you then need to imperatively add event listeners to and imperatively update, how is it much better than innerHTML?
1) Type safety for element props.
2) Autocomplete for element props.
3) IDE support such as refactors and jump to definition/jump to usages.
4) Proper syntax highlighting out of the box instead of the editor just saying "there's a string here".
5) A uniform pattern for defining custom components that work the same as primitives, rather than defining custom components as helper functions returning string fragments or something like that.
And so on. JSX has a lot going for it regardless of the semantics chosen. It's just a syntax that is very convenient for lots of kinds of tooling, and it's completely unopinated about the semantic context in which it is used.
These are definitely helpful, but what you are describing are all language tool features rather than features of JSX itself. 5 would be the exception, but that is just user preference of what kind of syntax one likes to write components with.
Well, yes. But OP was asking about what makes this better than `innerHTML`, and the obvious answer is that support for HTML programming embedded in JavaScript strings is generally bad while support for JSX is very good across all editors.
You can have JSX that produces DOM nodes or "light-weight element descriptions".
You can have imperative event listeners and updates.
These are two independent dimensions. I made UI framework called mutraction that produces real DOM elements from JSX expressions. It also updates any contents or attributes of those DOM nodes based on their dependencies without requiring imperative DOM interaction from application code.
https://github.com/tomtheisen/mutraction
Here's a click counter. `track()`, as you might guess creates a proxy so that reads and writes can be converted into dependencies.
I think the answer to that is probably "as good as soy, but with modern ergonomics". E4X was basically this and I think it's a much nicer way to build DOM trees than strings since you can't create invalid markup or concat partial tags. It also lets you reuse subtrees naturally where innerHTML makes that impossible.
innerHTML loses all local state, such as which elements have focus or where the cursor is in a text field. Back when React first came out and people were getting used to the idea of VDOM diffing, they had demos front and center about how by using those diffs to only change what needed to change, such local state wouldn't be lost.
This in theory could do something to copy that local state over, or diff the two DOMs directly without a VDOM (though from the sound of it, it probably doesn't).
If you work on a team of suitable size I would hesitate to not leverage a VDom. I trust myself to not trigger dumb reflows, but the way I see a lot of people using React this could be a perf nightmare.
I think that hits the real problem: it’s staffing and culture, not the tool. The 90th percentile site using a vDOM is also a perf nightmare, especially for anyone who doesn’t have a recent Apple device on a fast network, and that often has big impacts on usability and accessibility as well (dynamic loading sucks on screen readers unless you put way more time into it that most people do).
I was painfully reminded of that while visiting Europe last month on a data plan which was clearly deprioritized on partner networks - the old sites with .php in the URLs loaded in a few seconds and worked perfectly, but every time something failed to load in less than 5 minutes or partially loaded but didn’t work a quick trip over to webpagetest.org showed a lot of NextJS, React, et al. scripts trickling in because clearly a form with half a dozen fields needs 25MB of JavaScript.
The root cause is obvious: you get what you measure. If businesses prioritize Jira tickets closed per day, they’re going to get this soup of things promising to be easy to use for high developer velocity and they’re never going to get around to the optimizing it. If they’re trying to be able to hire as cheaply as possible, they’re going to look for the current tool boot camps are pushing and hire based on that, not looking for deeper knowledge of web standards or experience which costs more and shrink the candidate pool. If they’re looking for a safe choice, Facebook’s marketing means all of the big consulting companies will push React and few people will pause long enough to ask whether they’re building the same kind of apps it was designed to build (long session times, tons of local state being mutated, etc.) or whether they’re willing to invest the time needed to get it to perform reliably and well.
There are more and more frameworks that avoid the VDOM but still resolve this fine. As long as DOM mutation is handled by the framework (and not done ad-hoc), and as long as the framework has a mechanism for deferring those mutations until after all reads, then there shouldn't be a problem. This is the approach taken by SolidJS, Svelte, and even the new rendering model for VueJS.
In all these frameworks, mutations happen inside effects, and effects are scheduled such that they all happen at the end of the tick, avoiding reflows and thrashing.
I get the "no more imperative updates" dream. I've used these frameworks for probably a decade. I've mastered them.
Me personally, I prefer imperatively updating my DOM. I get completely fine-grained control over what's happening. I can architect it to be an extremely efficient machine. I can make it extremely easy to add/change/remove/fix features in my apps without forcing myself to think according to anyone else's opinionated methodology.
If you have little state, or simple uniform state, you can actually store it in the real DOM efficiently, as values of controls, or lists of similar DOM nodes under a common known parent. If most of your DOM is static, and you only need small bits of interactivity, React is an excessively heavy tool.
The farther you get into complex GUI territory, the more you want a declarative, functional approach, because it makes things simpler. The closer you are to a handful of controls with simple logic, the more you want to just imperatively tell them what to do, and leave the rest of the page alone, because it makes things simpler. We now just have better tools than jQuery for that.
there is no reason something imperative cannot be declarative. the war is one of style, not capability, so saying you gain "fine-grained control" is kind of meaningless, imo
This really just isn't true. If your state updates are at the component subtree level (like react) a vdom is a good choice. But, if you make your state changes more granular, you can get away with skipping VDOM entirely and work with just* regular dom nodes. Look at Solid or Svelte. No VDOM there, just pure granular updates.
*List reconciliation still has to happen, but you don't need to pull out an entire vdom. You just have to have some mapping between list items and their resulting DOM nodes.
TBF while Solid and Svelte don't use a VDOM on which they perform diffing, they still ultimately create a tree parallel to the DOM which is used to track dependencies.
Whether you like this project or not, your comment so completely misses the point. You are confusing the JSX syntax, which is what the author wanted by extracting it away from React, for all the React candy. This is a missing the forest for the trees kind of thing.
This mind numbing reliance upon layers of abstraction nonsense around state management is why I really don't like React. State management is ridiculously simple. State management, when done correctly, is the most primitive example of MVC with no abstractions needed.
State management is not simple. You have to constantly keep in sync two different piece of states (your data model and the UI). Making sure that when you modify some parts of your model then everything depending on that is also updated is one of the hardest things to guarantee. Fundamentally this is because it is non-local: you cannot tell what will change by just looking at the definition of what you're mutating.
You might be able to handle this while you're alone and you know everything about your codebase, but the moment you're working with someone else or in a team this will no longer be the case.
Returning actual DOM nodes entirely blunts the big advantage of JSX (and non-JSX libraries like Lit) - which is their immediate mode style API, and UI=f(state) model.
I feel like this is one of the leakiest abstractions in all of computing. There's a reason there's an entire cottage industry around react; how to stop things rendering multiple times, refreshing needlessly, etc.
Yeah, as much as I liked the idea of an “immediate mode API which is in fact retained under the hood which makes things both ergonomic and performant”, the reality is that React failed to deliver on that and every sufficiently big app ends up having performance problems that are then fixed by opting out of the immediate mode illusion.
I agree with the sibling comment that this really depends on the user. To take a different approach: JSX is just a different DSL to the createElement function call pattern (see Preact.h for example) and all of the benefits you’re describing come from the framework and runtime.
More concisely: JSX is just an alternate function call syntax with some useful applications.
For example at my last company we used JSX to make test data factories that had an XML-like look but were using a builder-pattern in the element creation that was able to make contextual decisions about what was in the final data. Nothing to do with React, DOM, or inability to express the same thing declaratively without JSX.
Thats really interesting, can you elaborate more?
For example did you use a specific JSX compiler? Was that written in house or used a 3rd party library?
I'm having fun using lit-html with vanillajs, after I saw a tweet from Marc Grabanski suggesting he didn't use the full Lit library. Admittedly I am then not taking advantage of all the reactivity goodness, but I also really dislike the decorators syntax and 95% of the time I just don't need the reactivity after the first render.
It works great! I was amazed at how you can do so much in templates, it's pretty much everything I could do in Vue templates, though a little more verbose.
I built my own `VanillaComponent` class, which has a mount() method which calls the render() function which I define on the child class.
My VanillaComponent class looks like this:
So I can write something like Then I can instance like so: The base class stores the root node (target), so later I can do to re-render, taking advantage of lit-html's "diffing" logic.However something I have not been able to solve with lit-html only, is when I compose parent and child components I have to do something like :
So the child component I need to explicitly call render() to get the TemplateResult for the parent template.But this means I can not do `childComponent.update()` because I don't know the root element of child component, since I did not mount it explicitly myself.
I mean technically because of the lit-html optimizations, I can do `.update()` on myDialog (the parent component) after any child component's props changes, and it will only re-render what is necessary... but let's say my child component has like 1000 cards... it seems very wasteful and it would be ideal if I could re-render only the child.
I wonder if there is a trick to get around that with just lit-html?
It's always worth checking the lit built in directives list for the one you've still missed (or at least it is for me ;).
I think in this case the ref() directive - i.e. https://lit.dev/docs/templates/directives/#ref - may be what you want. If it isn't exactly, reading how it's implemented would be my first step towards building something similar that is.
JSX in SolidJS directly returns DOM elements much like in the top part of this post, yet it does not have these disadvantages. It's true that strictly put it's not immediate mode like React and Lit are, but the framework is designed such that there's few practical downsides to that.
Nice, I love lit-html(1)!
I wanted to add my two pennies to the discussion. You are of correct that with that approach you lose the declarativeness but sometimes you don't need that, if the thing is mostly static.
I went this road many years ago for a project. The nice thing of this approach is getting rid of all the ugly DOM API but enjoying it's performance over innerHTML.
(1) I absolutely love lit-html but don't like the rest of the lit components framework. Luckily you can use it independently!
Svelte and SolidJs work pretty well without VDOM
UI is not a pure function of state[0], "UI state" is relatively stable and does not have to be recreated constantly when data input changes.
[0] https://blog.metaobject.com/2018/12/uis-are-not-pure-functio...
No you don't. It is inefficient and increases complexity. You then have to extract and keep track of state yourself where the platform/UI components could have done much of this themselves.