Skip to main content Accessibility Feedback

An intro to state-based UI with JavaScript

Yesterday, we looked at where traditional DOM manipulation starts to break down when building complex JavaScript apps.

When you find yourself making lots of UI updates that require you to be aware of the current state of other UI elements, it might be time to consider using a different approach: state-based UI.

Let’s dig in!

What is state-based UI?

With state-based UI, you hold data about the UI and how it should look in one or more JavaScript variables, called state.

(It’s called state because it represents your data at a particular state in time.)

Looking at our todo app as an example, we might instead store our todos as an array of items, like this…

let todos = [];

Then, we define a function that returns an HTML string with all of the markup for our UI.

In this example, if there are no todo items, we’ll return a paragraph element (p) and message. If there are todos, we’ll loop through each one and create a list item with the todo and a button to delete it from the list.

/**
 * Create the HTML based on the app state
 */
function getHTML () {

	// If there are no todos, show a message
	if (!todos.length) {
		return `<p><em>You don't have any todos yet.</em></p>`;
	}

	// Otherwise, render the todo items
	return `
		<ul>
			${todos.map(function (todo, index) {
				return `<li>${todo} <button data-delete="${index}">Delete</button></li>`;
			}).join('')}
		</ul>`;

}

We can then render our UI into the DOM using the Element.innerHTML property and getHTML() function.

// Render the UI
app.innerHTML = getHTML();

Now, we have a UI that’s driven by our state, in this case, the todos array.

Updating the UI with state-based UI

Whenever our state changes, we’ll again use the Element.innerHTML property and getHTML() function to update the UI.

In the addTodo() function, we no longer have to create various elements, append them, and selectively show or hide our message element. We’ll instead use the Array.push() method to add our todo, then run app.innerHTML = getHTML().

/**
 * Add todo to the list
 * @param {Event} event The event object
 */
function addTodo (event) {

	// Stop the form from reloading the page
	event.preventDefault();

	// If there's no field value, ignore the submission
	if (!form.todo.value) return;

	// Otherwise, add todo and rerender the UI
	todos.push(form.todo.value);
	app.innerHTML = getHTML();

	// ...

}

In the removeTodo() function, we can use the Array.splice() method to remove the todo from the todos array, and then again render a fresh UI.

/**
 * Remove todo items
 * @param  {Event} event The event object
 */
function removeTodo (event) {

	// Only run on [data-delete] items
	let index = event.target.getAttribute('data-delete');
	if (!index) return;

	// Otherwise, remove the todo and rerender the UI
	todos.splice(index, 1);
	app.innerHTML = getHTML();

	// ...

}

Instead of saving the HTML string to localStorage, we can save the todos array.

// Save the list
localStorage.setItem('todos-state-based', JSON.stringify(todos));

And instead of having to run a special SavedTodos() function, we can just set the todos array to any saved items when the page loads.

// Todo data
let todos = JSON.parse(localStorage.getItem('todos-state-based')) || [];

With state-based UI, we no longer concern ourselves with the current state of the UI. We only care about how it should look now given the data we have.

Here’s a demo you can play with on CodePen.

Issues with this approach

State-based UI has many nice advantages, but doing it with pure vanilla JS like this isn’t ideal either.

Re-rendering the UI with the Element.innerHTML property like we’re doing here is called clobbering the DOM (not the technical term, of course).

Re-rendering every element, whether it needs it or not, is bad for performance. It’s also potentially bad for accessibility, too, as it removes focus from the currently focused element.

We’re also manually re-rendering whenever we update our data. The more complex our app gets, the more likely it is that we forget to do that.

State-based UI libraries (from the big ones like React down to modern smaller ones) provide two major benefits:

  1. DOM diffing
  2. Reactive data

Tomorrow, we’ll look at both of those.