Plain Vanilla Applications

#

Project

When richer interactivity and dynamic state are needed, a single-page application is often a better fit than a multi-page website.

The suggested project layout for single-page applications is the same as for multi-page sites, except:

/public/pages
As there is only index.html there is no need for the pages folder.
/public/app
All of the views and routes for the application are in this folder, each implemented as a web component, and registered in index.js.
/public/app/App.js
As in the major frameworks, the application is bootstrapped from an App component. See the example below.
#

Routing

Without the assistance of a server to do routing, the only option is hash-based routing:

This behavior can be encapsulated in a routing web component:

An example single-page vanilla application that uses this routing component:

It makes use of the template pattern to avoid showing a broken application if scripting is disabled.

Adding additional route components to the /app folder is left as an exercise for the reader.

#

Entity encoding

A real-world application will often have complex markup in the web components, filled with variables based on user input. This creates a risk for Cross-Site Scripting. In fact, eagle-eyed readers may have noticed in the Passing Data example of the Components page that a XSS bug snuck in. By entering the following as a name, the output of list.js would have code injected:
<button onclick="alert('gotcha')">oops</button>

Go ahead and return to that page to try it ...

To solve this we need to encode dangerous HTML entities while plugging variables into HTML markup, something frameworks often do automatically in their templating layer. This html`` literal function can be used to do this automatically in a vanilla codebase:

The reworked list.js that uses this:

To learn more on using this function, check the html-literal documentation.

#

Managing state

Where state lives

State is the source of truth of what the application should show. It is the data that gets turned into markup on the page by the application's logic.

In web frameworks state is often carefully managed so that it lives outside of the DOM, and then rendered into the DOM using a view layer. Every time the state is modified, the view layer rerenders the current view based on the new state, updating the DOM behind the scenes to match the new view. In this design the DOM is just a view on the state, but does not actually contain the state.

In vanilla web development however, state and view are merged together inside of a web component. The component carries its state in attributes and properties, making it part of the DOM, and it updates its appearance based on changes in that state in a self-contained way. In this design the DOM ends up being the owner of the state, not just a view on that state.

Take for example a simple counter:

The <x-counter> component carries its state in the #count property, it provides an API for safely changing the state with the increment() method, and it always updates its appearance when the state is changed using the update() method.

Lifting state up

Putting state inside of web components as attributes and properties at first can seem simple, but when scaling this up to more complex hierarchies of components it quickly becomes difficult to manage. This means care must be taken to organize state across the component hierarchy in the right way.

Generally speaking, the state management principles laid out in the React documentation are sound and should be followed even for vanilla web development. Here they are once again:

  1. Group related state. If you always update two or more state variables at the same time, consider merging them into a single state variable.
  2. Avoid contradictions in state. When the state is structured in a way that several pieces of state may contradict and “disagree” with each other, you leave room for mistakes. Try to avoid this.
  3. Avoid redundant state. If you can calculate some information from the component's attributes or properties during rendering, you should not put that information into that component's state.
  4. Avoid duplication in state. When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can.
  5. Avoid deeply nested state. Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way.

Let's look at these principes in action by porting the React lifting state up tutorial example application to vanilla code:

The implementation is divided across two web components: <x-accordion> and <x-panel>. The state is "lifted up" from the panels onto the accordion, so that the accordion carries the state for both panels in a single central place.

Each of the two panels is stateless. It receives its state through the title and active properties. When it is active, it shows its children (inside of a slot). When it is not active, it shows a button labeled "Show". It always shows the title.

By contrast, the accordion is where the state for the panels actually lives:

What to pay attention to:

Finally, take a look at the original implementation of Accordion and Panel in React's tutorial:

Take note of how the state is organized the same across the React and vanilla implementations. The differences are in implementation details for state and rendering, not in how the application is structured.

Passing data deeply

While passing state deep into a hierarchy by handing it from parent components to child components via attributes or properties works, it can quickly become verbose and inconvenient. This is especially the case if you have to pass those through many components in the middle which have no need for that state aside from passing it to their child components, an anti-pattern colloquially known as "prop drilling".

Again we can take inspiration from how popular frameworks like React organize state, by adapting the concept of a context. A context holds state at a high level in the component hierarchy, and it can be accessed directly from anywhere in that hierarchy. The whole concept of a context is explained in the React passing data deeply tutorial.

To understand how to apply this concept in vanilla web development let us look at the ThemeContext example from the useContext documentation page. Here is an adapted vanilla version that uses a central context to keep track of light or dark theme, toggled by a button.

In this example a special web component <x-theme-context> is created, whose only job is to keep track of state, provide setters to update that state, and dispatch events when the state changes.

Some key take-aways:

The needs more context article does a deeper dive into the context protocol and the tiny-context implementation of it.

Up next

Go build something vanilla!

(Or keep reading on the blog.)