How I improved my programming speed by replacing React with plain JavaScript
January 29, 2021
No notes available for this episode.
Transcribed by Rugo Obi
At the top here, you see a bunch of imports from different modules which I use to divide up my code.
In a previous startup a few years ago, we were using React in the front end and then Ruby on Rails in the back end, but our development had become agonizingly slow.
So, we sat down and tried to figure out where it was that we were losing time. Where was it that we were spending the most hours to get the least amount of commercial value?
We came to the conclusion that we needed to tear React.js out. And we did, and then we noticed afterwards that development time had improved by about 30%.
And before we go any further, I want to emphasize that I freaking love React. This is not a React.js hate video. I've used it since 2016 and I think it's a great choice whenever you have a UI intensive or client-side state intensive web app. For example, a browser game, or maybe an email client. I just happened to believe that using React.js for your average commercial web application is complete overkill. It's like transporting a single postcard with an eighteen wheel truck. It makes no sense.
Today, I am going to present an alternative way of doing things, the no-framework framework.
This is just plain old JavaScript. I'm going to show you how to do this in a language-agnostic way using examples from Ruby on Rails and also PHP Laravel.
What are some of the concrete costs of using an SPA?
First and foremost is the doubling up of functionality.
We noticed that we had very similar code for routing both in the React world and in our API endpoints in the backend. Moreover, we saw that state management became a pain in the ass.
For example, this could diverge between the back and front end. React had one view of how the data was, and sometimes this wasn't in sync with how our backend saw things, in particularly the SQL database.
And sometimes, but I admit this wouldn't happen in all codebases, the code for formatting data and presenting it in the frontend sometimes had to be rewritten, both in the back end and the front end.
I'm talking about fields like dates or translation strings or financial figures with the decimal points in a certain place.
The reason why this got duplicated is because React generates some state itself, and then you need to format that state in React. At the same time, you might need to format that state, the API endpoint, for a fresh page load.
Of course, there are ways of doing things where you move all the presentational work to react, so that's why I say that this is only sometimes an issue.
The next cost that I became aware of was network issues.
When you have an API talking to an endpoint, there's increased complexity compared to just surfing generated HTML.
You have to think about things like whether or not the HTTP request actually succeeded and what you're going to do in your React UI, versus just relying on someone refreshing the page or whatever.
The next issue is that you've added a major dependency when you include React or the equivalent into your application. Now your team needs to maintain this, and all the dependencies of React and endure the architectural churn when React updates itself and then every other dependency breaks because they haven't updated at the same time. This has quite a high cost.
The single page way of doing things is non-default HTTP protocol, and things like the back button or search engine indexing stop just working, and you have to be more careful about how you do things in order to ensure that these things continue working for your end-users and for your website.
Lastly, when you have React as a dependency, your JavaScript payload size increases quite dramatically, so you can get a performance boost by dropping it in many websites.
Before I show you the code I'm going to quickly demo what the humble results are.
Here's a cookie notice powered by plain old JS, you can accept that, and then you can navigate throughout the site and see the rough speed of things.
And eventually, you can buy stuff and add to cart, there's a payment process that gets managed here.
Now let's walk through the code.
So, I'm going to open up the application.js
file, which is the main entry point from Webpacker’s point of view.
You can see here that there's some boilerplate that require three files, turbolinks
, and some Rails based stuff.
Turbolinks is one of the key components here. What it does is transparently hook into the page request cycle, and grab the next page without de-loading the current page, and then once that next page is available, it swaps out the old body for the new body and also ensures that the head tag is correct.
All in all, it provides a massive speed boost to HTML based web pages and can often obviate the need for an SPA.
Next, I have some polyfills.
Babel only covers ES6 features of JavaScript but not browser APIs, therefore, you need to include them separately in the JavaScript world.
So, here I import the fetch
libraries, it’s not available in all browsers. I define .forEach
for child node, I believe, and then I also include the data listing, which I use for autofill and so on.
Now we get to installOnDom
, which is the main entry point in my own personal no-framework framework.
Let's take a look inside this file.
At the top here, you see a bunch of imports from different modules which I use to divide up my code.
And then if we go down here, we have an event listener for turbolinks:load
. This is essentially the same as DOMContentLoaded
, except in the turbolinks world, there are a few tweaks needed to get turbolinks to replace regular JavaScript, not much.
And then we have all these module functions that get called to initialize the activity from these above modules here.
Now let's look at how one of these module functions is defined.
So, I'm going to choose cookieNotice
, and go inside here, and then I'm going to go to the bottom of the page and you can see, at the bottom of the screen rather, and you can see this default function that gets exported.
What does it do? Well, it checks some config
.
I use this to decide whether or not to show the cookie notice, this enables testing, we'll get to that later. And if it's enabled, I call this other function showCookieNotice
, you can see it defined here.
Essentially this contains a big if-statement, switching on whether or not there is a cookie available for advancedTrackingCookiesEnabled
.
I have the answer to the question and the cookies pop up. And if they haven't, then we display the pop up using setCookieNoticeDisplay
to flex
otherwise set to none
.
The kind of big picture here is that there's a pattern I'm using just to either show an element or hide an element based on some logic. That's part of the very very simple no-framework framework.
You can see here how the function setCookieNoticeDisplay
is defined. It looks for the cookieNotice
element on the screen, and if it finds it - it might not be on every page so I kind of always check for NULL
- then it sets the style CSS to whatever the value parsing the function is.
Next, I'm going to show you how the functionality is connected from this JavaScript world to my HTML.
So, here you see two document.querySelector
calls, and they are looking for the data-accept-cookies
attribute, and one will be set to true
, and the other will be set to false
, you can see here.
Let's have a look at the actual HTML here, I have another buffer with that, I believe. Yeah, here we go.
So you can see here's a button to I accept
, and it hits the root_path
blah blah blah. And then here's the data
attribute being generated via Rails with accept-cookies: true
, and here we see Technical settings
, accept-cookies: false
. Well, it gets transformed into that.
Once we have those particular elements, we addEventListener
on click
to each of those elements, and call different functionality depending on whether or not the acceptButton
, or the rejectButton
was clicked.
Here we giveCookieAnswer (‘true’)
, here giveCookieAnswer (‘false’)
.
One critique I have of my own code, going back and looking at it now, is that over here for the accept and reject buttons, I use data attributes in order to target the elements, which JavaScript hooks into.
However, if you look down here in this function setCookieNoticeDisplay
, I'm using an ID attribute cookie-notice
. And this is kind of inconsistent, I'd rather have all IDs or all data attributes.
If I had to choose between the two, I would go with all data attributes because that has the advantage of being able to select multiple elements at once.
For example, I can have three different elements on the screen with data-accept-cookies
even if I had no true
/ false
value there. And I could target them and it would be legal HTML.
But it's impossible for me to have three different elements on page with the ID cookie-notice
, that's invalid HTML. Therefore, you get more flexibility with data attributes, therefore I'd go for it.
I also would avoid going with CSS classes because I like to keep them exclusively for styling. That gives me and other people on the team confidence that if they change some CSS class, no functionality will break, it will only have visual effects.
Now that you've seen basically how the code works, let me show you a unit test.
So, I'm going to go to the related file here on Vim, and you can see a test; cookieNotice.spec.js
.
I'm using the jest test framework here, I believe, and at the top of the file, I import the cookieNotice
function module, just like I did over in installOnDom
. Yeah, you can see the file in the pane there.
And then I import resetDom
which is a function I have to clear cookies and ensure that every test starts with the same kind of reset state.
Next, I have a function, loadPageHtml
, which is the unit test equivalent of moving to a new page, except it's much lighter.
My entire HTML document is just a div
with the ID cookie-notice
, and the key data attributes here. data-accept-cookies= “true”
and “false”
.
It's kind of the MVP of the HTML structure that my JavaScript file was meant to act upon. You can see the data accept stuff there.
Then I replace the document and the test with that little piece of HTML there, using this line here, and then I call my code from the cookieNotice
module just the same as what happened in installOnDom
.
I just do it manually, instead of waiting for the addEventListener
because I want my test to run straight away.
Let's scroll down and look at one of the actual tests.
So, here we have the first one. It sets cookie and goes away when accepted
.
So, how do I run this test?
Well, essentially, I manually simulate someone clicking on the acceptButton
. I grab that button, and then I call the click
event on it. Which is the same thing as the browser would cause to happen if someone did click on it. And then I check some expectations, that the trackingCookieValue
is true
, that the cookieNoticeDisplayValue
or whatever is none
.
And then I load the page again, and check the cookie value and kind of persist across that page load.
Scrolling down to another test here, you can see that I mock some of the browser behavior, for example, window.alert
, since I call alert, and then I call the rejectButton
and ensure that window.alert
was called with the expected text.
The exact details of all this aren't that important, the big picture is that all of this is plain JavaScript and pretty easy to understand.
You don't have to grok React or whatever the framework of the day is - in a particular version -in order to be able to write and debug this code. It's just the common basic JavaScript that everyone shares and that's what I like about it.
For the sake of completion, I'm going to show the integration tests that test the same behavior.
This is written in the Rails world, and you can see here that there's some stuff with cookie notice enabled, js: true
.
I set_js_variables
, this is a function I use to kind of inject JavaScript variables into the JavaScript world from my backend, in order to control whether or not the cookie-notice
shows. I don't want it to show in every single test because that will be annoying and slow down things, but I do want it to show on this test, therefore I set skip to false
.
And then, yeah, within the cookie-notice
thing, click on ‘I accept’
and then check whether or not trackings were actually made.
I use an object here in JavaScript called trackingsMade
, and it has keys like googleAnalytics
which can be set to true
or false,
and then I just evaluate the JavaScript from my Rails test, run some assertions there.
And this just ensures that everything is working together quite nicely.
This episode has gone on long enough, so I'm going to continue covering this topic next week.