After creating RawJS, I'm never touching React again.
Tags: javascript, webdev, react, typescript
Published: 2024-01-02 16:04:09

**EDIT: I'm the author of RawJS. Everyone, please cool your jets. Omitting this on the main article was an oversight, and my authorship of this library was stated in my bio from the get-go.**

**EDIT 2: I'm considering locking the comments in this article, rewriting it, and linking forward. Some people are taking serious issue with the way I've framed parts of it. It's steering the discussion in a way that I don't think is productive for the community. I'll leave this here but if the turbulence in the comments continues, I'll lock & rewrite.**

Way back in October of 2012, TypeScript 0.8 was released. I was working on a midsize JavaScript project at the time. The day it was released, I read over the initial spec, and after about 10 minutes of playing with it, I become so convinced that it would be the future that I began rewriting my entire app in TypeScript. The benefits over standard untyped JavaScript were that dramatic.

I get the same feeling with [RawJS](https://www.squaresapp.org/rawjs/).

[RawJS is a tiny library](https://github.com/squaresapp/rawjs) that makes vanilla JavaScript app development vastly more ergonomic. It's not just the latest web framework of the day to compete with React, Vue, Svelte, or whatever else. RawJS is different.

RawJS is a visceral demonstration of why the whole premise of frameworks themselves is perhaps a bit misguided. It suggests that most apps would likely be better off using vanilla JS and adopting certain programming patterns.

That's a pretty bold statement, I know. But I implore you to look into the mindset and the thinking behind what brought RawJS to life.


## React Tends To Poison Projects With Crippling Levels Of Complexity

I don't believe I'm going out on too much of a limb by stating that React is overcomplicated. After all–Svelte was created specifically because of this. 

React leads to bloated apps. Case in point––I recently was given the task to oversee the development of a React app whose complexity had spun out of control. I ended up throwing out the entire app, and reconstructing the whole thing with RawJS, using a team who had never touched RawJS, or had even done much direct DOM manipulation at all. Within a couple weeks, the team was up to speed and now the app is about 90% smaller than the previous React app. No, that's not a typo.

We've come to the point now where React is the "no one gets fired for buying IBM" choice. Problem is––people *should* be fired for buying IBM. This is the metaphor for the individual who can't be bothered do adequate needs analysis, defaults to herd mentality, and blindly goes with whatever most other people are using out of some combination of fear and laziness.

The degree of React's overcomplexity becomes a lot more clear once you've had the pleasure to develop an app using RawJS-accelerated vanilla JavaScript. RawJS calls into question the necessity of props, state, hooks, JSX, forced inheritance from specific bases (React.Component), Virtual DOM, automatic data binding, and essentially everything that React does.

The bloat of React and the restrictions it imposes (such as forbidding you from editing the DOM directly) are all centered around trying to maintain the integrity of it's Virtual DOM system, which I see as a solution to a domain-specific problem that was specific to facebook.com, that well-built apps simply don't have.

The alleged performance disadvantages of direct DOM manipulation are _vastly_ overblown. As a matter of fact, when done properly, direct DOM manipulation usually results in improved performance. This is because you're able to get precise control over how the DOM gets updated. It allows you to go in with a scalpel update tiny regions of the DOM as necessary. Virtual DOM is the opposite of this. It's a blunt tool that runs a complex diffing algorithm over huge swaths of DOM subtrees in order to automatically compute what needs to get updated.

The usefulness of automatic data-binding and reactivity are also over-blown. Assuming your code is well organized, building your app so that the DOM updates as a consequence of data changes doesn't appear to result in any less code than just building the app to update the DOM explicitly when and where necessary. But the difference with the former is that it forces upon you a massive hard-to-debug black-box, and forces conformance into their layer of bureaucracy. It's hard to appreciate the benefits of getting away from this until you've put together a well-built vanilla JS app (such as with RawJS!).

## Why is no one talking about Anonymous Controller Classes?

Anonymous Controller Classes (ACC) are a pattern that needs to get more attention. They're one of the key ideas that transform a vanilla JavaScript app from a sloppy mess into something coherent and beautiful.

The basic premise of an ACC is to create an object that is loosely connected to a single element in the DOM, and whose garbage collection lifecycle is equal to the lifecycle of the connected element. It's an advancement over and above inheriting from HTMLElement, which is another option (but one which I don't like, for reasons I'll leave to another article).

Consider the following code:

```typescript
class SomeComponent {
	readonly head;
	
	constructor() {
		this.head = document.createElement("div");
		this.head.addEventListener("click', () => this.click());
		// Probably do some other stuff to this.head
	}
	
	private handleClick() {
		alert("Clicked!")
	}
}
```

ACCs are classes that create a single .head element, possibly other nested elements, connect event listeners, assign styling, etc. They have methods which are typically event handlers or other helper methods. You then instantiate the component, and add the component's .head element to the DOM:

```typescript
const component = new SomeComponent();
document.body.append(component.head);
```

The class is considered "anonymous" because you can discard your instance of the component as soon as its attached to the DOM. The instance of the class will be garbage collected as soon as the element is removed from the DOM and garbage collected, because the DOM is the only thing that owns a reference to it. For example:

```typescript
class SomeComponent {
	readonly head;
	
	constructor() {
		this.head = document.createElement("div");
		this.head.addEventListener("click', () => this.remove());
		// Probably do some other stuff to this.head
	}
	
	private remove() {
		// Remove the component's .head element from the DOM,
		// which will by extension garbage collect this instance of
		// SomeComponent.
		this.head.remove();
	}
}
```

The great thing about ACCs is that they impose essentially no restrictions. They can inherit from anything (or nothing). They're just an idea––you can mold them to behave however you like.

Of course, there are many scenarios when you might want to get the ACC associated with a particular element. For example, imagine iterating through the ancestor elements of the this.head element, and getting the ACCs associated with it in order to invoke some public method. There is a lightweight library called [HatJS](https://github.com/squaresapp/hatjs) which is designed to improve the ergonomics around working with ACCs.

**EDIT: I'm the author of HatJS. And "Anonymous Controller Classes" is a term I originated. It's a pattern that emerged during experimentation, though I doubt I'm the first person to discover it, because the concept is quite obvious. Like JSON before it had a name. You don't need to use HatJS with RawJS. A lot of people are building vanilla JavaScript apps (or "vanilla TypeScript apps" for the pedantic) and having good success just by creating custom elements that inherit from HTMLElement, effectively merging the element and the controller into the same entity. I've built a few apps with that method and have decided that ACCs are better for reasons I can cover in a future article.**

## Improving document.createElement() has surprisingly powerful implications

Although this article attempts to make the strongest case possible for using the DOM APIs directly, one area where these APIs absolutely fall flat is in the area of constructing complex DOM hierarchies with attributes, styling, and event attachments. This part of the DOM API is so verbose that without some outside help, your code is going to be about 10x longer than it needs to be. This is where RawJS comes in.

RawJS was designed for exactly one purpose. It gives a 10x improvement in the ergonomics around document.createElement(). Call functions and get hierarchies of HTMLElement instances. It does nothing else. There's no weird background magic. You may not think this sounds terribly impactful. But you would be wrong in that assessment.

It turns out that for the last 15 years of ideation around framework patterns, we didn't need Virtual DOMs, reactivity, data-binding pre-compilers, or any other wild science projects. We needed the anonymous controller class pattern and a better way to create HTMLElement instances.

Using these two techniques, I can say for certain that I will never willfully use React, or any other competing framework again. This class of framework simply doesn't have enough to offer over and above what can already be done with JavaScript, to warrant the enormous weight and bureaucracy they impose.

So what does RawJS code look like? Calls to the RawJS creator functions follow the form:

```typescript
const htmlElement = raw.div(...parameters);
```

The powerful ergonomics come from the breadth of what is acceptable as a parameter (typed as a `Raw.Param` in RawJS).

Parameters can be strings, numbers, booleans, arrays, functions, DOM Node instances, calls to `raw.on("event", ...)` (which create portable event attachments), and pretty much anything else. RawJS always does what you'd expect.

I'm not going to reiterate the awesome things you can do with RawJS to construct hierarchies. This is covered in detail in the quickstart.

The main idea is that because nearly anything can be a Raw.Param, you're able to create mini libraries of functions that generate lists of Raw.Params and return them. The level of code reuse you can achieve with this is unprecedented. Again, its hard to appreciate until you've actually worked with it. I hate to make the comparison to LISP / closure, but there are similarities.

## The best CSS-in-JS solution I've seen

What good is an HTML element hierarchy constructor without being about to also construct the corresponding CSS? RawJS also has the best-in-class CSS-in-JS solution that does things I've not seen in any other solutions. For example RawJS has full support for CSS that is scoped to a particular element.

```typescript
const anchor = raw.a(
	// This constructs CSS within a global style sheet,
	// and the rules below will be scoped to the containing
	// anchor element.
	raw.css(
		":focus", {
			outline: 0
		},
		":visited": {
			color: "red"
		}
	),
	raw.text("Hyperlink!")
);
```

There is quite a bit of other things that can be done with RawJS's CSS-in-JS solution. This article isn't meant to be a RawJS tutorial, but if you're looking for that kind of thing there is a quick-start and a demo app.

## Using the DOM as your state manager is actually good.

A common objection we get is, where do you store your application state? The answer is to use the DOM as your state manager. 

Before you shudder at this, remember how tailwind pioneered the idea of dropping millions of class names on HTML elements, effectively recreating the equivalent of inline CSS, which for years was thought of as an anti-pattern, but the devs were insistent that it's actually good, and now everyone is doing it? The same idea applies to using the DOM as your state manager.

The way how everyone has decided to build apps these days is to start with some kind of data structure which is deemed to be your "source of truth", and then you need to awkwardly project this into a UI somehow. Doing it another way is considered "naive" or even an anti-pattern. However, I would like to suggest that having two separate representations that need to be kept in sync, itself is an anti-pattern.

Trying to use some framework to declaratively map your data into DOM has lead to vast amounts of increased complexity. The problem with this technique is that having two separate representations of the same thing is automatically going to be more bloated than a hypothetical alternate technique where there is only one such representation.

It turns out that it's actually much better if you make your ACCs accept input data in order render themselves. Then, you have some kind of save function that inspects the state of the DOM to produce a saveable chunk of data. This way, your source of truth doesn't have to synced with the DOM because your source of truth IS the DOM. 

Inspect the following code example:

```typescript
class FormComponent {
	readonly head;
	private readonly firstNameInput;
	private readonly lastNameInput;
	
	constructor(firstName: string, lastName: string) {
		this.head = raw.form(
			this.firstNameInput = raw.input({ type: "text", value: firstName }),
			this.lastNameInput = raw.input({ type: "text", value: lastName }),
			raw.button(
				raw.text("Save"),
				raw.on("submit", () => this.save())
			)
		);
	}
	
	private save() {
		const firstName = this.firstNameInput.value;
		const lastName = this.lastNameInput.value;
		SomeDatabaseSomewhere.save({ firstName, lastName });
		alert("Saved!");
	}
}
```

See? You just store the values right in the DOM. In this case, we were using the value of a text input, but you could also store the data as HTML attributes, class names, or whatever makes sense.

Granted, there are cases where you need to store state that can't be broken down into strings, numbers, and booleans. I've also seen some cases where state is stored within properties within the ACC. Do whatever works for you.

## Communication between components

There are situations where you may need to update multiple components in response to one action, or to more simply send messages between ACCs. HatJS can help with this.

Remember, ACCs create a kind of hidden controller hierarchy. You have your typical DOM hierarchy of elements, but only some elements are the head elements of an ACC. So this creates its own ACC hierarchy that is a strict subset of the whole DOM element hierarchy.

[HatJS](https://github.com/squaresapp/hatjs) has functions to traverse the ACC hierarchy, and quickly capture the ACC instances that may or may not be sitting behind an element.

```typescript
class ParentComponent {
	readonly head;
	constructor() {
		this.head = raw.div(
			new ChildComponent().head
		);
		
		// Call Hat.wear() to define the object as a "hat"
		// and make it discoverable by HatJS
		Hat.wear(this);
	}
	
	callAlert() {
		alert("Hello!");
	}
}

class ChildComponent {
	readonly head;
	constructor() {
		this.head = raw.div(
			raw.on("click", () => {
				// Hat.over finds the "Hat" (or the ACC) that exists
				// above the specified element in the hierarchy.
				// And passing ParentComponent gives you type-safe
				// tells HatJS what kind of component you're looking
				// for, and also gives you type-safe access to it.
				Hat.over(this, ParentComponent).callAlert()
			})
		);
	}
}
```

In addition to `Hat.over()`, there is also `Hat.under()`, `Hat.nearest()`, and other methods to find other ACCs of specific types that may or may not exist in the DOM relative to specified elements.

## Get excited!

So, have I convinced you to set your React app on fire and rebuild your life's work with vanilla using RawJS? [Here's the RawJS website](https://www.squaresapp.org/rawjs/) if you'd like to get started. The repo for RawJS is [here](https://github.com/squaresapp/rawjs), and the repo for HatJS is [here](https://github.com/squaresapp/hatjs)