Your template language is killing the web.

The web is arguably the most powerful thing humans have ever created. It’s my favorite platform to build for, and it has been for over a decade.

It used to be simple to build things for the web. In some ways this is still the case, but modern realtime apps are driving the complexity factor to new and uncomfortable heights.

I love the kinds of apps we can now build (and use), but I question the extra complexity. Making web development harder also makes it less accessible. This troubles me because the democratization of development is one of the many things I love so much about the web.

You need knowledge of more tools and technologies than ever to build a web app that feels modern. In contemplating this shift, much of the added complexity stems from how we handle the part of the application a user interacts with — the view layer.


Across frameworks and technologies, view rendering is a destructive action. This one misstep requires going down many complicated paths in an effort to build more compelling apps. To explain this, let’s step back a few years and examine a more traditional approach to web development.

Consider this template, written in the ERB template language:

Here we iterate over a collection of user objects, printing out a greeting that contains the name of each user.

Just by observing this template we understand something about the view that will be rendered from it. For one, we know that each p tag represents information about a user. In our case, the user’s name.

Now, if we render this template based on the following data:

We get this result:

What does this rendered view represent? Right, it’s hard without context.

Once a template is rendered we lose all knowledge of how it was rendered and what the rendered view represents. If User 1 changes their name, we have no way to update the node for that user. Our only choice is to render the entire template again based on the new state of our user data.

This is the crux of complexity in modern web development.


So what’s the alternative? Let’s look at an example. Here’s a new template:

Just like the ERB template, it’s also possible to reason about this template and understand what data it represents. We can literally read the HTML and walk away with an understanding of the underlying data model. To render in a non-destructive way, this knowledge must be retained during rendering. One way to do this is to manipulate the view externally.

Here’s a bit of Ruby code we can use to render this template:

This code finds the user scope in the template and transforms it to look like the data being applied. When rendered with our user data, the rendered view looks like this:

The rendered view is basically the same as before, but all the knowledge of underlying state is maintained. In fact, this knowledge is even extended. Each node reveals both a schema of our data model and knowledge of specific user state (i.e. each user’s id and name).

What’s more is that the logic to transform this view isn’t really view logic; there’s nothing DOM-specific about it. Instead, our logic is a declarative statement written in context of our data.

The real view logic is contained within apply, which maps the declarative statement into a series of instructions that transform the template to match the user state. Really the underlying view could be anything; here it just happens to be an HTML document.

Views are just representations of data.

To reinforce this key idea, look at what’s left when we remove the HTML decoration (i.e. the non-data bits) from the rendered view:

If we group these by type, we’re left with the exact same user state we started with. Our rendered view just mirrors the data. All views do, but this view also contains knowledge of exactly what the data is.

Why is this a big deal? Good question. Let’s look at non-destructive view rendering in context of building modern, realtime web apps.

Updating rendered views with new state.

Let’s pick back up on the use-case I laid out a moment ago. If you recall, when a user’s name changes we want the rendered views in each client’s web browser to immediately reflect the change. How do we go about this, preferably without rendering the view again?

Remember how the backend logic can be evaluated in any context, not just in context of a view. In this case, we can evaluate the logic in a way that allows us to capture the instructions and send them to the client.

Here’s an example of the instructions that would be pushed to the client when changing the name of the first user (represented as JSON):

At a high level, these instructions describe how to update the state of a view. They are also portable and can be evaluated anywhere. We can easily send the instructions to clients concerned with the new state and let a client library take care of updating the rendered view.

For our purposes today just assume that we have some mechanism of sending these instructions to the right clients (which is a very solvable problem). A Javascript library on the client can evaluate the instructions in context of the rendered view, effectively updating the underlying view state.

Here’s the result of applying the above instructions:

Our view now represents the new state, and we didn’t have to render the view again, only update previously rendered nodes! This is also a traditional backend-driven approach, as we didn’t move any part of our application to the client. Instead, the backend renders the initial view, keeps track of the state rendered on each client, and sends updates as state changes.

This is a super elegant solution to an otherwise difficult problem.

Mapping user-interaction to mutation.

Because of the knowledge contained in rendered views, we can also convert a user’s interaction with a rendered view into a mutation in state.

As an example, say we have a form used for creating a user:

Reasoning about this example we quickly come to the conclusion that when the form is submitted, a new user will be created by the backend. The scoped form tag essentially marks mutable front-end state.

The client library can be designed to handle this for us. When the form is submitted, the client library can intercept the event and inform the backend of the attempted state change via a websocket. Once the change is accepted, it can be broadcast to other connected clients concerned with the state.

Many interactions within a browser can automatically be interpreted as being mutable interactions. For those that can’t (e.g. drag and drop) the developer can simply write a bit of Javascript that watches for the custom interaction and maps it to a mutation in app state.

This gives clients a modern, realtime user-experience without moving a single part of our application to the client. An important side note is that everything about the application still works without websocket support or the client library being present. It just ceases to be realtime.

Call it progressive enhancement or whatever you like; I call it simple.

Conclusion

The approach outlined here enables us to build web apps that:

1. Are built in a traditional backend-driven style.
2. Update their rendered views to stay in sync with the current state.
3. Understand how to map user interactions into changes in state.

All of the above is possible without moving to the client, without transpiling to Javascript, and without rendering the view again. Client and server stick to their traditional roles, and we don’t create additional complexity.

This is how I’ve wanted to build for the web since I first started more than a decade ago. These ideas will soon see the light of day as a realtime user-interface layer on top of the Pakyow web framework.

For more details, you can watch a screencast where I build a backend-driven realtime app in 10 minutes without writing a single line of Javascript.

The web is getting fun again.

Did you enjoy this article? Please pass it along to a friend or two. I’d love to hear from you. You can drop me a line or tweet me @bryanp.

Like what you read? Give Bryan Powell a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.