8 months ago
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.
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.
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.
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
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
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
I have the answer to the question and the cookies pop up. And if they haven't, then we display the pop up using
flex otherwise set to
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.
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
accept-cookies: false. Well, it gets transformed into that.
Once we have those particular elements, we
click to each of those elements, and call different functionality depending on whether or not the
acceptButton, or the
rejectButton was clicked.
giveCookieAnswer (‘true’), here
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
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;
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
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
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
true, that the
cookieNoticeDisplayValue or whatever is
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.
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.
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
And then, yeah, within the
click on ‘I accept’ and then check whether or not trackings were actually made.
trackingsMade, and it has keys like
googleAnalytics which can be set to
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.