before semicolon logo Before Semicolon

Reactive DOM — Finally!

Reactive DOM — Finally!

Reactive DOM — Finally

DOM API should not be the reason to avoid building with Vanilla JavaScript

As a web developer, if you stop to think about web development, you will realize that all UI libraries and frameworks are just trying to fix the cumbersome of working with DOM and creating reusable code. You can find everything else built into the browser or JavaScript already.

For example, let’s create a simple counter widget.

We can start by defining the HTML like so:

<div class="vanilla-count">
<p>count: <span class="count-value">0</span></p>
<button type="button" class="count-down">-</button>
<button type="button" class="count-up">+</button>
</div>

Without adding any styling, let’s add behavior to it with JavaScript:

const vanillaCount = document.querySelector('.vanilla-count');
const [countDisplay, countDownButton, countUpButton] = vanillaCount.children;
const [countSpan] = countDisplay.children;

let count = 0;

countUpButton.addEventListener('click', () => {
count += 1;
countSpan.textContent = count;
})

countDownButton.addEventListener('click', () => {
count -= 1;
countSpan.textContent = count;
})

The problem

With this, we face a few problems:

  • HTML and JavaScript code are decoupled: nothing couples these together to ensure that changing one wouldn’t break the other.
  • Custom reactivity and DOM manipulation: To make it work, you must add your reactive behavior with event listening and DOM manipulation. This is a simple example, but it would be a living hell in a more complex scenario.
  • Reusability: if you want to reuse this in multiple places, it does not come with additional management and refactoring to avoid conflicts.

We can solve the reusability and decoupling issues easily with functions. Something like this:

const createCountWidget = (parent) => {
const vanillaCount = document.createElement('div');
vanillaCount.className = 'vanilla-count';
vanillaCount.innerHTML = `
<p>count: <span class="count-value">0</span></p>
<button type="button" class="count-down">-</button>
<button type="button" class="count-up">+</button>
`

const [countDisplay, countDownButton, countUpButton] = vanillaCount.children;
const [countSpan] = countDisplay.children;

let count = 0;

countUpButton.addEventListener('click', () => {
count += 1;
countSpan.textContent = count;
})

countDownButton.addEventListener('click', () => {
count -= 1;
countSpan.textContent = count;
})

parent.appendChild(vanillaCount)
}

By moving everything inside a function, we ensure everything works together and reuse it as often as possible by calling it with a parent node where it must be added.

createCountWidget(document.body)

This can even be a web component to solve the same issues:

class CountWidget extends HTMLElement {
count = 0;

constructor() {
super();

this.innerHTML = `
<p>count: <span class="count-value">0</span></p>
<button type="button" class="count-down">-</button>
<button type="button" class="count-up">+</button>
`;
}

countUp = () => {
this.count += 1;
this.querySelector('.count-value').textContent = this.count;
}

countDown = (evt) => {
this.count -= 1;
this.querySelector('.count-value').textContent = this.count;
}

connectedCallback() {
const [, countDownButton, countUpButton] = this.children;

countUpButton.addEventListener('click', this.countUp);
countDownButton.addEventListener('click', this.countDown);
}
}

customElements.define('count-widget', CountWidget)

We would have our custom HTML tag to use anywhere we like…

<count-widget></count-widget>

Web components give us some reactivity from props, but as you can see, state handling and DOM manipulation inside the component are still required to make things work.

Now what?

This is why people use libraries like React, Angular, and many others. They solve such problems by providing out-of-the-box APIs and a big ecosystem that allows us to build anything fast and reliable.

If you could solve the reactivity in the DOM, you wouldn’t need these libraries unless you want to lock yourself in an ecosystem or framework.

For example, what if we could do the following:

const CountWidget = () => {
const [count, updateCount] = state(0);

const countUp = () => updateCount(prev => prev + 1)
const countDown = () => updateCount(prev => prev - 1)

return html`
<div class="markup-count">
<p>count: <span class="count-value">${count}</span></p>
<button type="button" class="count-down" onclick=${countDown}>-</button>
<button type="button" class="count-up" onclick=${countUp}>+</button>
</div>
`
}

CountWidget().render(document.body)

The only thing new in the above code is state and html APIs; everything else is Vanilla JavaScript. It leaves everything up to you as a web developer.

Or if web components were just as simple as:

class CountWidget extends WebComponent {
initialState = {
count: 0
}

countUp = () => this.setState({
count: this.state.count() + 1
})

countDown = () => this.setState({
count: this.state.count() - 1
})

render() {
return html`
<div class="markup-count">
<p>count: <span class="count-value">${this.state.count}</span></p>
<button type="button" class="count-down" onclick=${this.countDown}>-</button>
<button type="button" class="count-up" onclick=${this.countUp}>+</button>
</div>
`
}
}

customElements.define('count-widget', CountWidget)

This is all possible with Markup in a roughly 6kb package requiring no build and code compilation. Simply add a link to the top of your HTML page and get:

  • Reactive templates with state handling;
  • Small and fast solution for DOM manipulation and reusable code;
  • Server-side rendering for templates;
  • Web components API simplified;

I work with all these popular UI libraries, but occasionally, I want to do something simple without distancing myself too far from Vanilla JavaScript.

So, I created Markup and redefined what reactivity and Web Components could be and do without installing a big opinionated package.

I prepared an essential training, but you can always check the docs for examples and more details.

We have many great solutions; everyone can find what works for them. I wish something like Markup could be defaulted in browsers so we don’t have to split ourselves into different ecosystems and fight over which is better.

I created Markup so it could work for anyone alongside any library and framework while keeping it simple down to 3 essential APIs, leaving everything else up to everyone’s imagination.

Let me know what you think!


Leave a Comment