Getting Out of the Rabbit Hole SPA with Modern Rails

Original author: Jorge Manrubia
  • Transfer
TL; DR: The SPA trail is dark and full of horrors. You can fearlessly fight them ... or choose another path that will lead you to the right place: modern Rails.



I remember thinking that Rails was focusing on the wrong goal when DHH announced Turbolinks in 2012. Then I was convinced that instant response time during user interaction was the key to superior UX. Due to network delays, this interactivity is only possible if you minimize network dependence and, instead of network calls, maintain most of the state on the client.

I thought it was necessary for the applications I was working on. And with that opinion, I tried many approaches and frameworks for implementing the same template: Single-page applications (SPA). I believed that SPA is the future. A few years later, I’m not sure what the future is, but I definitely want to find an alternative.

Rabbit hole SPA


A single-page application is a JavaScript application that, once downloaded, gets full control without having to reload the page: rendering, receiving data from the server, processing user interaction, updating the screen ...

Such applications look more native than traditional web pages, where the application depends on the server . For example, if you use Trello, you may notice how quickly cards are created.

Of course, great responsibility comes with great force. In traditional web applications, your server application includes a domain model and business rules, data access technology for working with a database, and a controller layer to control how HTML pages are assembled in response to HTTP requests.

C SPA is a bit more complicated. You still need a server application that includes your domain model and rules, a web server, a database and some kind of data access technology ... and a bunch of extra things on top:

For the server:

  • An API that meets your customer's data needs
  • A JSON serialization system for exchanging data with the client and a caching system that supports this

For the new JavaScript client:

  • Template system for converting data to HTML
  • Presentation of your domain model and rules
  • The data access layer in the client application for transferring data to the server
  • System for updating views when data changes
  • A system for linking URLs and screens (I hope you won’t use one address for everything, it doesn’t look like a web application)
  • A system for gluing all the components necessary to display screens and obtain data for this
  • New templates and architecture for organizing everything when
  • System for error handling, logging, exception tracking, etc.
  • System for generating JSON for server requests
  • Automated Testing Framework Supporting Your SPA
  • An additional set of tests for writing and supporting
  • An additional set of tools for assembling, packaging and deploying a new application

In sum, with SPA you will have another application for support. And a new set of problems to solve. And note, you cannot replace one application with another. You still need the server application (now it will render JSON instead of HTML).

If you have never worked with SPA, you can underestimate the difficulties that you will encounter. I know this because I made the same mistakes in the past. Render JSON? I can handle it. A rich domain object model in JavaScript? That sounds funny. And, you know, this framework will solve all these problems. Great cake!

Wrong.

API and data exchange.


Communication between your new application and the server is a complex issue.

There are two opposing forces:

  • You will want to make as few server requests as possible in order to improve performance.
  • Converting a single record to JSON is simple. But mixing large models of various records to minimize the number of queries is not at all. You will need to carefully develop serialization logic to optimize the number of database queries and maintain performance at the level.

In addition, you need to think about what to download, and when to do it for each of their screens. I mean, you need a balance between bootstrap, what you need immediately and what can be loaded lazily, and develop an API that allows you to do this.

Some standards may help here. JSON API to standardize JSON format; or GraphQL, to select only the necessary data, as complex as required, in one query. But not one of them will save you from:
  • The elaboration of each data exchange
  • Implementing queries to select data efficiently on the server

And both of these aspects represent a sufficient amount of additional work.

Boot time


People are used to associating SPAs with speed, but the truth is that getting them to load fast is not so easy. There are many reasons for this:

  • An application needs data before rendering something, and it takes time to parse a sufficiently large amount of JavaScript.
  • Beyond the initial HTTP request to download the application, you usually need to make one or more requests to get the JSON data needed to render the screen.
  • The client must convert JSON to HTML in order to show at least something. Depending on the device and the amount of JSON to convert, this can introduce noticeable delays.

This does not mean that it is impossible to force the SPA to load quickly. I just say that it’s difficult and you should take care of it, because it won’t come along with the SPA architecture.

For example, Discourse, an Ember-based SPA, has fantastic load times, but among other things, they preload a large amount of JSON data as part of the initial HTML so as not to make additional requests. And I note that the Discourse team are crazy in the good sense of speed and their skills are well above average. Think about it before you can easily reproduce the same in your SPA.

An ambitious solution to this problem is isomorphic JavaScript: render your start page on the server and quickly give it to the client, while in the background of the SPA it loads and gets full control when ready.

This approach requires running the JavaScript runtime on the server, and it is also not without technical problems. For example, developers should consider SPA loading events as the loading process changes.

I like the opportunity to reuse the code in this idea, but I have not seen an implementation that would allow me to go in the opposite direction. In addition, this page rendering process seems very funny to me:

- Server: request to the server API
- Server: request to the database
- Server: generate JSON
- Server: convert JSON to HTML
- Client: display initial HTML
- Client: load SPA
- Client: parse initial HTML and subscribe to DOM events

Could you just request the data from the database, generate the HTML and start working?

This is not entirely fair, because you will not be SPA and because most of this magic is hidden behind the framework, but it still seems to me wrong.

Architecture


Developing applications with a rich user interface is difficult. This is all because this is one of the problems that inspired the emergence of an object-oriented approach and many design patterns.

Managing client status is difficult. Traditional websites usually focus on single-responsibility screens that lose state when they reboot. In SPA, the application is responsible for ensuring that all state and screen updates during use are consistent and pass smoothly.

In practice, if you started writing JavaScript in small portions to improve interaction, then in SPA you will have to write tons of additional JavaScript code. Here you should make sure that you are doing everything right.

There are as many different architectures as there are SPA frameworks:

  • Most frameworks differ from the traditional MVC template. Ember was initially inspired by Cocoa MVC, but changed its programming model quite a lot in recent versions.
  • There is a tendency that developers prefer components rather than the traditional separation of the controller and presentation (some frameworks, such as Ember and Angular, have moved to this approach in recent versions). All frameworks implement some kind of one-way data binding. Double-sided binding is discouraged due to side effects that it may contribute.
  • Most frameworks include a routing system that allows you to map URLs and screens, and determines how to instantiate components for rendering. This is a unique web approach that does not exist on traditional desktop interfaces.
  • Most frameworks separate HTML templates from JavaScript code, but React mixes HTML-generation and JavaScript and does it quite successfully, given its widespread use. There is also a hype around embedding CSS in JavaScript. Facebook, with its Flux architecture, has pretty much influenced the industry, and containers like Redux, vuex, etc. are heavily influenced by it.

Of all the frameworks I've seen, Ember is my favorite. I adore his consistency and the fact that he is rather stubborn. I also like his programming model in recent versions, combining traditional MVC, components, and routing.

On the other hand, I am strongly against the Flux / Redux camp. I saw so many smart people using them that I made every effort to study and understand it, and not once. I can't help but shake my head in frustration when I see the code. I do not see myself happy while writing such code.

Finally, I can't put up with the mix of HTML and CSS in components full of JavaScript logic. I understand what problem this solves, but the problems that this approach brings do not make it worth it.

Let's leave personal preferences, the bottom line is that if you choose the SPA path, you will have a very difficult problem: to create the architecture of your new application correctly. And the industry is quite far from agreeing on how to do this. Every year, new frameworks, templates, and versions of frameworks appear, which slightly changes the programming model. You will need to write and maintain a ton of code based on your architectural choice, so think about it properly.

Code duplication


When working with SPA, you will probably encounter code duplication.

For your SPA logic, you will want to create a rich model of objects representing your domain area and business logic. And you still need the same thing for server logic. And it's a matter of time before you start copying the code.

For example, suppose you are working with invoices. You probably have an Invoice class in JavaScript that contains a total method that sums up the price of all the elements so you can render the value. On the server, you will also need the Invoice class with the total method to calculate this cost in order to send it by e-mail. You see? The Invoice client and server classes implement the same logic. Code duplication.

As said above, isomorphic JavaScript could mitigate this problem, making it easier to reuse code. And I say to level, because the correspondence between the client and the server is not always 1-to-1. You will want to be sure that some code never leaves the server. A large amount of code makes sense only for the client. And also, some aspects are simply different (for example, the server element can save data in the database, and the client can use the remote API). Reusing code, even if possible, is a complex problem.

You can bet that you do not need a rich model in your SPA and that you will instead work with JSON / JavaScript objects directly, distributing the logic across the components of the UI. Now you have the same code duplication mixed with your UI code, good luck with that.

And the same thing happens if you want templates for rendering HTML between the server and the client. For example, for SEO, how about generating pages on the server for search engines? You will need to re-write your templates on the server and make sure that they are synchronized with the client. Again duplication of code.

The need to reproduce the logic of patterns on the server and client, in my experience, is the source of the growing misfortune of programmers. To do this once is normal. When you do this for the 20th time, you will clutch your head. Having done this for the 50th time, you will think about whether all these SPA pieces are needed.

Fragility


In my experience, developing good SPAs is a much more complicated task than writing web applications with server generation.

First, no matter how careful you are, no matter how many tests you write. The more code you write, the more bugs you will have. And SPA represents (sorry, if I push hard) a huge pile of code for writing and supporting.

Secondly, as mentioned above, developing a rich GUI is complicated and translates into complex systems consisting of many elements interacting with each other. The more complex the system you create, the more bugs you have. And compared to traditional web applications using MVC, SPA complexity is just insane.

For example, to maintain consistency on the server, you can use restrictions in the database, model validation, and transactions. If something goes wrong, you are responding with an error message. In the client, everything is slightly more complicated. So much can go wrong because too much is happening. It may be that some record is saved successfully, and some other record is not. Perhaps you went offline in the middle of some kind of operation. You must make sure that the UI remains consistent and that the application is recovering from an error. All this is possible, of course, only much more complicated.

Organizational Challenges


It sounds silly, but to develop a SPA, you need developers who know what to do with it. At the same time, you should not underestimate the complexity of SPA, you should not think that any experienced web developer with the right motivation and understanding can write a great SPA from scratch. You need the right skills and experience, or expect important mistakes to be made. I know this because it is exactly my case.

This is perhaps a more important challenge for your company than you think. The SPA approach encourages highly specialized teams instead of teams from generalists:

  • SPA frameworks are complex pieces of software that require countless hours of experience to be productive. Only the people in your company who spend these hours on the code will be able to support these applications.
  • SPA frameworks require well-thought-out and productive APIs. Meeting these requirements requires a completely different set of skills than those that require work with SPA.

The chances are that you will find yourself with people who cannot work with SPA, and who cannot work on the server side, simply because they do not know how.

This specialization may be ideal for Facebook or Google and their teams consisting of several layers of engineering troops. But will it be good for your team of 6 people?

Modern Rails


There are 3 things that go into modern Rails that can change your mind about developing modern web applications:
  • one of them is Turbolinks and this is a brain explosion
  • the other is an old friend who is overlooked today: SJR responses and simple AJAX requests for rendering
  • and the last one was added recently: Stimulus

It is difficult to understand what it is to apply any approach without playing with it. Therefore, I will make a few references to Basecamp in the following sections. I have nothing to do with Basecamp, except as a happy user. As for this article, this is just a good live example of modern Rails that you can try for free.

Turbolinks


The idea behind Turbolinks is simple: speed up your application by completely replacing page reloads with AJAX requests that replace the `` element. The inner witchcraft that does this work is hidden. As a developer, you can focus on traditional server-side programming.

Turbolinks is inspired by pjax and went through several revisions.

I used to worry about its performance. I was wrong. The acceleration is huge. What convinced me was how I used it in the project, but you can just try the trial version in Basecamp and play with it. Try creating a project with some elements, and then navigate through them by clicking on sections. This will give you a good idea of ​​what Turbolinks looks like.

I do not think that Turbolinks is simply amazing with its novelty (pjax - 8 years). Or its technical sophistication. It amazes me how a simple idea can increase your productivity by an order of magnitude compared to the alternative to SPA.

Let me highlight some of the problems that it fixes:

  • Data exchange. You do not have it. No need to serialize JSON, create APIs, or think about data requests that satisfy customer needs in terms of performance.
  • Initial load. Unlike SPA, this approach stimulates fast loading times (by design). To render the screen, you can get the data that you need directly from the database. And efficiently querying data from relational databases or caching HTML are well-addressed problems.
  • Architecture: You do not need a complex architecture to organize your JavaScript code. You only need to focus on the correct architecture of your server application, which you still need to do when using SPA.

The MVC on the server, in the version used by Rails and many other frameworks, is much simpler than any of the templates used for the rich GUI architecture: get a request, work with the database to satisfy it, and display the HTML page as an answer.

Finally, the limitation that is always replaced has a wonderful effect: you can focus on the initial rendering of pages instead of updating certain sections (or updating some conditions in the SPA world). In the general case, he just does everything.

  • Code duplication. There is only one view of your application that lives on the server. Your domain model, its rules, application screens, etc. No need to duplicate concepts in the client.
  • Fragility. Compared to SPA, JavaScript for working on your pages and its complexity are reduced to a small fraction, and therefore the number of errors. In addition, you can rely on atomic operations on the server using database transactions, restrictions, and validations.

Note that I am not talking about naming problems, but about fixing them. For example, GraphQL or SPA rehydration are super smart solutions for very complex problems. But what if, instead of trying to find a solution, you put yourself in a situation where these problems do not exist? This is a change in approach to the problem. And it took me years to fully appreciate the ability of this approach to solve problems.

Of course, Turbolinks is not a hassle-free silver bullet. The biggest problem is that it can break existing JavaScript code:

  • Turbolinks ships with its custom “page load” event, and existing plugins that rely on regular page loads will not work. Today there are better ways to add behavior to the DOM, but legacy widgets will not work if they are not adapted.
  • JavaScript code that modifies the DOM must be idempotent because it can be run multiple times. Again, this invalidates a lot of existing JavaScript.
  • The speed is excellent, but it’s not quite like in SPA, which can handle some interactions without loading the server. I will talk more about compromises later.

AJAX rendering and SJR answers


Remember when rendering HTML through Ajax was trending 15 years ago? Guess what? This is still a great tool in your arsenal:
  • Getting the HTML fragment from the server and adding it to the DOM feels superfast (100ms fast).
  • You can render HTML on the server, which allows you to reuse your views and retrieve the necessary data directly from the database.

You can see how this approach is felt in Basecamp by opening your profile menu by clicking on the top right button:



Opens instantly. From the development side, you do not need to worry about JSON serialization and the client side. You can simply display this fragment on the server, using all the features of Rails.

A similar tool that Rails has included over the years is server-side JavaScript (SJR) responses. They allow you to respond to Ajax requests (usually form submissions) with JavaScript that is executed by the client. It provides the same benefits as AJAX rendering of HTML snippets: it runs very fast, you can reuse the code on the server side, and you can directly access the database to create a response.

You can see how this happens if you go into Basecamp and try to create a new todo. After you click “Add todo”, the server will save the todo and respond with a JavaScript fragment that adds a new todo to the DOM.

I think many developers today look at AJAX rendering and SJR responses with contempt. I remember that too. They are a tool and, as such, can be abused. But when used correctly, this is a terrific solution. Let you offer great UX and interactivity at a very low price. Unfortunately, like Turbolinks, they are difficult to evaluate if you have not yet fought SPA.

Stimulus


Stimulus is a JavaScript framework published a few months ago. It does not care about rendering or JavaScript-based state management. Instead, it's just a nice, modern way to organize JavaScript, which you use to add HTML:

  • It uses MutationObserver to bind behavior to the DOM, meaning it doesn’t care how the HTML appears on the page. Of course, this works great with Turbolinks.
  • It will save you a ton of boilerplate code for attaching behavior to the DOM, for attaching event handlers to events, and for placing elements in the specified container.
  • It aims to make your HTML code readable and understandable, which is nice if you have ever encountered the problem of finding out which part of JavaScript is acting on this damn element.
  • It encourages persistence in the DOM. Again, this means that he doesn't care how the HTML is generated, which is suitable for many scenarios, including Turbolinks.

If you accept the Rails path, your JavaScript will focus on modifying the server-side HTML code and improving interaction (with a bit of JavaScript). Stimulus is designed to organize such code. This is not a SPA system and does not pretend to be one.

I used Stimulus in several projects, and I really like it. It eliminates a bunch of boilerplate code, it is built on the latest web standards and reads very beautifully. And something that I especially love: now this is the standard way to do something that still had to be solved in every application.

Game of compromise


Turbolinks is usually sold as “Get all the benefits of SPA without any inconvenience.” I do not think this is completely true:

  • Applications built using modern Rails look fast, but SPA will still respond faster to interactions that are server-independent.
  • There are scenarios in which SPA makes more sense. If you need to offer a high level of interactivity, you need to manage a large number of states, execute complex logic on the client side, etc. SPA framework will make your life easier.

Now development is a game of compromise. And in this game:
  • Modern Rails allows you to create applications that are fast enough and look great.
  • For a huge variety of applications, Rails allows you to implement the same functions with less code and less complexity.

I believe that with Rails you can get 90% of what SPA offers with 10% effort. In terms of performance, Rails kills SPA. As for UX, I think that many developers make the same mistake as me, assuming that SPA UX is unsurpassed. This is not true. In fact, as discussed above, you better know what you are doing when creating your SPA, or UX will actually be worse.

Conclusion


I watch companies massively accept SPA frameworks and see countless articles on how to do fancy SPA-style things. I think there are many “uses of the wrong tool for the job,” since I firmly believe that the types of applications that justify the use of SPA are limited.

And I say they justify it because SPAs are complex. In any case, I hope I convinced you of this. I'm not saying that it’s impossible to create great SPA applications, or that modern Rails applications are great by definition, just one approach is super complicated and the other is much simpler.

While preparing this article, I came across this tweet:



It made me laugh because I would choose the first options if the alternative was not justified. He is also a representative of the kind of thinking of developers who loves complexity and thrives on it, even down to thinking that other people with different criteria are crazy.

After many years, I realized that complexity is often a choice. But in the programming world, it is surprisingly hard to choose simplicity. We value complexity so much that accepting simplicity often makes us think differently, which is by definition difficult.

Remember that you can get rid of trouble. If you choose the SPA path, make sure it is justified and you understand the problems. If you're not sure, experiment with different approaches and see for yourself. Perhaps Facebook or Google, on their scale, do not have the luxury of making such decisions, but you probably can.

And if you are a Rails developer who left Rails many years ago, I recommend you return to it. I think you will be delighted.

Also popular now: