Skip to main content Accessibility Feedback

How to manage the borders of complexity in JavaScript projects

Last week, I wrote about the edges of complexity.

I’m at the border of this uncomfortable place where manual DOM manipulation approaches are becoming increasingly complex and hard to manage, but state-based UI and reactive data introduce their own complexities and make some things that are currently easy with manual DOM manipulation a fair bit harder.

Today, I wanted to share how I ended up approaching this project to make that complexity easier to manage.

Let’s dig in!

Project recap

I’m working on an app for a service industry company. The page I’m building in this app displays project details unique to each customer.

Some of the common elements on the page are pre-rendered HTML, but the stuff that’s unique to the customer and project are rendered with JavaScript and an API call.

Most of the page is only rendered once, and then selectively shown or hidden with things like toggle tabs and accordions. That makes traditional DOM manipulation very appealing. It’s more performant and “just works.”

There are a few sections on the page, though, where we need to ask for more information from the user, then update the UI.

Let’s say we don’t have a phone number for the customer, yet. We might display a section like this…

<h2>We don't have a phone number for you</h2>
<button>Add a phone number</button>

The user clicks the button, a modal opens to get that information, and we update it. Now, that section should look like this…

<h2>Phone number added</h2>
<button disabled>Add a phone number</button>

There are a handful of those throughout the UI, and each one has more UI changes than what I’ve included in the example above.

Lots of little classes to toggle text color, attributes to affect interactivity, language changes. It adds up!

The problem with most reactive state-based UI libraries

For a project with lots of dynamic UI driven by user interaction, I might normally reach for a state-based UI library. In the past, I’ve written about a mostly vanilla JS way to work with Preact.

The problem with most libraries like that is that any rendered HTML that uses your state becomes reactive by default.

That’s not bad on the surface, but it means that you need to add properties for modals, tabs, and accordions to your state object as well. Simple DOM libraries will get broken every time the UI renders again.

For example, you could rely on the dialog element and HTMLDialogElement.showModal() method to toggle a modal with traditional DOM manipulation

<button data-modal="#my-modal">Open Me</button>

<dialog id="my-modal">
	...
</dialog>
document.addEventListener('click', function (event) {

	// Make sure a modal button was clicked
	let target = event.target.getAttribute('data-modal');
	if (!target) return;

	// Get the modal
	let modal = document.querySelector(target);
	if (!modal) return;

	// Open the modal
	modal.showModal();
});

But with a state-based UI library, you now need to manage the open/closed state of the dialog element, and code a bunch of custom HTML and CSS to manage the backdrop, overlay, scrolling, and so on.

This adds a bunch of additional complexity to the project while reducing the complexity of some other stuff, so you end up just as bad off as you were at the start.

The solution: a hybrid approach

One thing I love about my own state-based UI library, Reef, is that it decouples reactive state and automated rendering.

You create a store() that notifies you whenever data changes, and have a render() function that updates and diffs the DOM. There’s a component() that automates rendering whenever the store data changes, but its optional.

What I ended up doing…

  1. I added all of my data to a reactive store.
  2. I render() an HTML string into the DOM when the page initially loads and the API returns data.
  3. I listen for changes to the store (it emits a custom event).
  4. If a specific property has been updated, I selectively manually re-render just the section of the DOM that needs it (something Reef makes much easier than other libraries, too).

Here’s a stripped down example of the state…

// Our state
let data = store({
	phone_number: null
});

And the templates used to render the initial UI.

// The missing info template
function missing () {

	// If there's no phone number
	if (!data.phone_number) {
		return `
			<h2>We don't have a phone number for you</h2>
			<button>Add a phone number</button>`;
	}

	// If there is
	return `
		<h2>Phone number added</h2>
		<button disabled>Add a phone number</button>`;

}

// The main UI, including the missing info section
function mainUI () {
	return `
		<h1>Hey there!</h1>
		<div id="missing-info">${missing()}</div>
		<p>Something something other API based info...</p>`;
}

I initially render the UI like this…

render('#app', mainUI());

Then, I listen for state changes, and selectively update the #missing-info section only if needed.

let phone = data.phone_number;
document.addEventListener('reef:store', function (event) {

	// If the phone number hasn't changed, do nothing
	if (event.detail.phone_number === phone) return;

	// Otherwise, re-render the #missing-info section
	render('#missing-info', missing());

	// Update the phone variables
	phone = event.detail.phone_number;

});

This let’s me use mostly vanilla DOM manipulation for interactive components, with state-based UI where it’s useful.