Ludovic Frank - Freelance developer

Feedback on Symfony-UX Turbo after intensive production use

ionicons-v5-k Ludovic Frank Jan 16, 2023
123 reads Level:

Hello there,

If you're a regular reader of this blog, you'll know that I've been talking a lot about Symfonfony-UX/Turbo for a few months now, but I've never given any feedback on it.

The reason is simple: before giving an opinion, it's necessary to really test the thing... and the best way is to use it with real users over a minimum period of time.

To date, my first application with Turbo and Stimulus has been in production for a little over a month now and has seen a few thousand users, so I've been able to get some feedback on the user experience (many thanks to them 😁).

Symfony-UX, what's it all about?

Let's keep things simple, and start with a look at the presentation site

Symfony-UX is a set of libraries, JavaScript and PHP enabling developers to create a superb user experience.

At the heart of it all is Stimulus, the super-simple JavaScript framework from the world of Ruby on Rails.

Around Stimulus, you'll find a number of libraries, each with their own specific uses. For example, you can integrate React, VueJS or ChartJS.

Then, to take things a step further, there's Turbo, which gives a "SPA" (Single Page Application) experience to our Symfony application (well, here it's Symfony, but Turbo works with any back end in fact).

Turbo and Stimulus are part of the same set of libraries, but are totally independent of each other, that set being Hotwired.

In my opinion, the great strength of this set of components is its simplicity: you spend more time thinking about what you're developing, rather than how you're developing it...

How does Turbo work?

At its most basic, Turbo "suppresses page loading" in the browser...

Let me explain:
When a link is clicked, instead of loading the page, Turbo will load only the page content, then replace the page content in the DOM.

The consequence of this is that the browser doesn't need to reinterpret the JavaScript and CSS (and if they're not cached, to re-download them) and if the server responds quickly (in my case 40ms) then there's immediacy in the interface display.

It's therefore possible to use server-side templating tools (on Symfony, Twig) to create an SPA-type application.

You should also be aware that there's a caching system in the history of what Turbo does. In order to return quickly to a previous page, Turbo will briefly present the "cached" version of this page before displaying it again.This can have consequences, for example if you've left a modal open, the behavior may be ... bizarre, another case may be when an alert has remained in the cached version, it will reappear briefly before being replaced by the version returned by the server.

Turbo doesn't require JavaScript to function
That's one of the great things about Turbo: you don't need to write JavaScript to take advantage of everything I'm talking about here.

Of course, it is possible to enhance the user experience with JavaScript, and that's where Stimulus comes in.

Turbo Frame

Think of it like a good old iframe: what happens in a turbo frame, stays in a turbo frame, and doesn't affect the rest of the page.
So you can divide your page into several smaller chunks, and when you click on a link in one of these chunks, only that part of the page will be updated - the rest will remain untouched.

Imagine, for example, a form for adding comments; when you've validated your comment, the server sends back an HTML alert saying: Your comment has been added, well only this part of the page will be updated... And if you have a good eye, in what I've just described, at no point have you written any JavaScript, it's just HTML and your back-end language (PHP in the case of Symfony).

Turbo frames are great, but now let's imagine that at the same time you need to update another part of the page... How do you go about it?

Turbo Stream

In addition to Turbo Frame, there's Turbo Stream...

What's the difference?
Turbo Frames are used to change a part of the interface, but what happens if, for example, you also want to change an element of the navbar, or update the number of comments posted by a user in real time? In his little user bar at the top of the interface?

That's where Turbo Streams come in, rather than sending back in the response only the content that replaces the comment form. Well, in the same response we send back two Turbo Streams, one to replace the form and another to update the number of comments in the navbar.

It's simplified, but basically that's it...

Go further with Turbo streams
What would be crazy is if, when a server-side event occurs, such as another user adding a comment, the page would be updated for all users currently on that interface... but how?
Turbo stream is fully compatible with technologies such as Mercure.

All you have to do is "subscribe" to a URL, and as soon as black emits an event, the users concerned who have subscribed to this event will see their interfaces updated in real time.

It's forbidden to write JavaScript in the <body>.

It's very important to stress here that with Turbo, it's forbidden to write JavaScript in the page body.

Quite simply, because the body is replaced by Turbo, and if any JavaScript ends up in the server response, the browser will simply execute it every time.

After a while, you can imagine that there will be a memory leak problem?

(On conventional sites and applications, the JavaScript state is cleaned up each time the page is changed, so it's difficult to do anything, since the next time the page is loaded, everything will be "reset" anyway).

So, what do we do to add JavaScript???

Stimulus, the basis of Symfony-UX

Stimulus is not only the basis of Symfony-UX, it's also Turbo's best friend, allowing us to add JavaScript to completely enhance our application.

The great thing about Stimulus is that it lets you do great things without writing mountains of JS.

How Stimulus works

In fact, it's very simple: it just looks at the DOM all the time, and when it sees an element that contains a "stimulus-controller", it loads the controller in question from the HTML. We can pass variables to the controller (what we'd call "props" in the React world).

A controller can have "targets", which are elements located within the controller element (children) that can be easily targeted from the controller with a variable predefined for us, the variable ends up with a name like this: "this.targetNameTarget".

This variable simply contains the DOM element in question, nothing more, nothing less... (without JQuery?)

When Turbo updates the DOM or part of it, Stimulus looks to see which control it should load. Once loaded, it fetches the connect() method and executes it.

Wondering what a Stimulus controller might look like?

As I said, it's very simple... we'll see what this controller does later in the article... ?

My first Symfony-UX/Turbo application

Now that we've seen how Turbo and Stimulus work, what do we do?

I've never been a big fan of "doing code for code's sake", so I need a concrete project...
After devouring SymfonyCast's Turbo and Stimulus training courses.
I asked myself "what useful project, that solves a real problem, could I create?"

In my circle of friends, I'm lucky enough to have entrepreneurs as friends, and among those friends is Patrice Marchand, owner of the Frères Marchand restaurant in Nancy.
During our long discussions, he told me how his restaurant takes reservations, and we agreed that we could do better...

The idea of a booking tool to help restaurant owners avoid "no-shows" was born, and for me, this app was the perfect way to get started with Symfony-UX/Turbo.

As I strolled through the Parc de la Pépinière in Nancy, the project germinated in my head... then, in July 2022, I started writing the code, and in August of the same year, we had a first version that was functional, but could be improved upon.a, the application had to be used in production, with real customers and a real restaurant, in order to get feedback, or is it stuck? What can we improve? What do we change? In short, the "agile method" as it's often called.

The idea is very simple: after configuring a few details such as opening times, the restaurant provides a link enabling its customers to make a reservation. During the reservation process, a credit card number is requested, in order to protect the restaurant from "no-shows".

(When a customer makes a reservation without showing up and without warning, no payment is made during the reservation process).

Since December 10, 2022, this application has been the method for making reservations at the Frères Marchand restaurant... To date, thousands of people have reserved their tables using this tool?

(just goes to show how quickly an idea can be adopted, if it's well conceived).

Now, let's take a look at a few interfaces and deconstruct them, to understand how they work.

A turbo frame like no other

Turbo Frame delimitation

Basically, a Turbo frame, as explained above, is like an iFrame: it lets you change part of the page, but this doesn't affect the browser's main URL.
In the screenshot above, the Turbo frame is in blue, and in this case I need to modify the browser URL when it changes, because it's actually the central element of the page.

This kind of special case can be handled with Turbo, in this case I set the "data-turbo-action" attribute to "advance" and in addition to that, I had to listen to Turbo events in order to set the "data-turbo-action" attribute to "advance".In addition, I had to listen to Turbo's events to see when it was doing a restore (going back to a page already visited) in order to modify its behavior.

In fact, Turbo emits JavaScript events such as

  • turbo:before-cache: Issued before Turbo caches a "page" in order to move on to the next, this event allows, for example, alerts left open to be closed and therefore not displayed if the user returns to that location
  • turbo:before-fetch-request: lets you know when Turbo is about to make a request to the server, useful for adding leaders to the request, for example.

And many more... available in the Turbo documentation.

The part in green is just a simple "div", in fact it only changes when the user changes the language. To do this, in the turbo frame (in blue) there's an HTML call to a Stimulus controller that will change the text of the div to green, allowing me to use Symfony's translation system?

The part in red is a Stimulus controller that surrounds a form, in which there are two visible inputs.
When a customer selects the date and the number of place settings, the Stimulus controller takes care of giving this information to the server, which responds with the HTML to be displayed (the yellow part). What's interesting here is that the yellow part is simply inserted into the dom with a simple innerHTML, nothing too exotic.

Finally, once the user has clicked on a button to set the desired time, a third invisible "input" is defined and the complete form is sent to the server.
This has the effect of replacing the entire Turbo Frame (the blue part).

There are additional animations provided by the form's Stimulus controller, but it's possible to do without them, so I won't dwell on them.

A stimulus controller that ticks boxes

In the back-office, the restaurant can choose the days of the week it's open, and the opening hours for each day.

Checbox with Stimulus controller

This interface works with checkboxes, but can you see yourself ticking the boxes one by one?

On this interface, you can see buttons like "check all" or "uncheck all", and in fact behind these buttons is the Stimulus controller I mentioned earlier.

We can see that this controller takes a "value".

This value is the class of checkboxes to be checked or unchecked, and we find the definition of this value in the HTML

Contrôleur Stimulus avec valeurs

This simple Stimulus controller means you don't have to click everywhere over and over again, while still being able to be very picky about opening times.

Buttons that are actually Turbo Frames

In the interface for closing certain hours for certain days, we needed something very simple and visual, so here's the interface:

Button in a turbo frame

Here, the form circled in orange is in fact a Stimulus controller. When an element is changed, there's a "RequestSubmit" which updates the page immediately (without reloading it).

In this Stimulus controller there's "action", the "change" of form elements, i.e. the start day and the number of days.

Here, each button (even those not circled in green) is a Turbo frame. When clicked, the server performs the basic modification and returns the HTML of the button in red or green, depending on whether the schedule is closed or open.
In the HTML, apart from the Turbo frame, the button is simply a classic anchor with the URL of the route that updates the schedule in the database.

Here's how it looks once the buttons are clicked:

Button with closed timetable (restaurant reservation)

Stripe and Stimulus

Stripe with Stimulus

As explained above, in this kind of application, all JavaScript is in the form of a Stimulus controller.

Stripe is no exception, and to integrate it I simply used the wrapper provided by Stripe to access Stripe Element from a webpack module.

In this kind of case, you just need to be careful that the external library doesn't add JavaScript to the page, and also remember that when Stimulus loads a controller, it calls its connect() method, and when it unloads it, it uses the disconnect() method.

Useful if the external lib requires a method to be called when the element it created is destroyed.

Another special case with Stimulus

Here, I won't dwell on it too much, as the case is the use of Intl-input with Stimulus, and I've already dealt with it in this article.

By default, the memory leak is enormous with these two libraries, and the browser quickly crashes, which is why I devoted an article to it at the time.

Conclusion

Turbo and Stimulus are recent technologies, so the basic idea is superb and it works perfectly most of the time.
Problems can arise when using external libraries, and it sometimes takes a little time to find a solution, but in my case, there always was one.

I particularly appreciated these technologies, as they enabled me, once I'd gone through the documentation and understood the principles, to take an idea from idea to "project in production" in just a few months.

(The initial development took a month, but the business world is slow and it takes time to train everyone to use the tool before putting it into production).

To sum up, it enabled me to spend more time wondering what features would be best rather than asking myself "how am I going to do this", and development time was drastically reduced.

I think that for all projects of this kind, Turbo and Stimulus will have a big place in my toolbox.

Have a great week?