Introduction
So, we’ve got around 430 lines of code sitting in one HTML file. While it works perfectly fine, it’s not very maintainable. We need to break this up into smaller, more manageable pieces. This has classically been called “separation of concerns”.
Broadly, this just means splitting our code into different files, each with a single responsibility. In the past, this could be as simple as splitting our code into three files: one for our HTML, one for our CSS, and one for our JavaScript. Let’s start there.
Separation by Language
One of the best practices you’ll hear about when you’re trying to learn how to CSS good is “separation of concerns.” The idea is that your HTML should only contain information about your content, and all of your styling decisions should be made in your CSS. —Adam Wathan, CSS Utility Classes and “Separation of Concerns”
Since the majority of the code will be the same here, There won’t be many snippets. Instead, I’ll just show you the file structure. But, we will be changing one function. To simplify our file structure, I will be using an external API to get our data. This also shows that these concepts work outside of our current project. I also added some minimal error handling to our fetch function.
new db function
const db = async () => {
const response = await fetch("https://dummyjson.com/products")
if (!response.ok) {
alert("HTTP-Error: " + response.status)
console.error(response)
return Nope("badFetch")
}
const data = await response.json()
return data
}
So, here’s our new file structure:
├── index.html
├── public
│ ├── App.js
│ ├── diana.avif
│ ├── style.css
│ └── favicon.ico
Now, we can link our CSS and JavaScript files in our HTML file.
<!DOCTYPE html>
<link rel="stylesheet" href="style.css" />
<script src="App.js" defer></script>
1-classic
So, instead of over 400 lines of code in one file, we have around 50 lines of HTML, 70 lines of CSS, and 300 lines of JavaScript. This is a lot better, but it’s still not ideal. We have a lot of code in our JavaScript file, and it’s not very clear what each part does. Let’s break this up into smaller pieces.
Our New Architecture
It is what I sometimes have called “the separation of concerns”, which, even if not perfectly possible, is yet the only available technique for effective ordering of one’s thoughts, that I know of. This is what I mean by “focusing one’s attention upon some aspect”: it does not mean ignoring the other aspects, it is just doing justice to the fact that from this aspect’s point of view, the other is irrelevant. It is being one- and multiple-track minded simultaneously. —Edsger W. Dijkstra, Notes on Structured Programming (1970)
There are a million different ways to do this, and I’m not going to pretend that mine is the best. But, I will try to explain some of my decisions. I plan on splitting my files up into three folders: components, features, and pages. I will explain the purpose of each of these folders soon, but first let’s see what the file structure looks like:
├── index.html
├── public
│ ├── App.js
│ ├── diana.avif
│ ├── favicon.ico
│ ├── style.css
│ ├── components # essential building blocks of our app
│ │ ├── Link.js
│ │ ├── Product.js
│ │ ├── render.js
│ │ ├── Router.js
| │ ├── Routes.js
│ │ └── store.js
└── │── features # additional functionality
│ │ ├── cart.js
│ │ ├── search.js
│ │ └── theme.js
└── │── pages # the actual view templates
│ ├── About.js
│ ├── Cart.js
│ ├── Home.js
│ ├── Nope.js
│ ├── ProductPage.js
│ └── Products.js
Hopefully, my comments and naming conventions make it clear what each folder is for. But, if not, I’ll explain it here.
Components, Features, and Pages
For anyone who says “I move files around until it feels right”: This may be alright as a solo developer, but is that really something you would do in a cross-functional team of 4 developers with a total of 5 cross-functional teams in a company? At a higher scale of teams, it becomes tricky to “just move files around without a clear vision”. —Robin Wieruch, React Folder Structure in 5 Steps
Components are the core pieces of our app. Generally, these refer to reusable visual elements, but I am also including anything that is necessary for our app to function. I want this folder structure to remain throughout every future iteration of this project, and I hope this will make more sense as we go along.
Features in our application are anything extra that we want to add. While our app wouldn’t be much without these features, they aren’t essential to the functionality of our app. We don’t need different themes or a search bar, but they are nice to have. So, anything like that will go in this folder.
Pages are the actual files that our router will use to render our views. I am intentionally mirroring the structure of projects like Next.js and SvelteKit which we will be exploring in future articles. Although, considering Remix and recent changes, maybe I should have named this folder “app” or “routes” instead.
A House of Cards
The holy grail is having a set of scripts download immediately without blocking rendering and execute as soon as possible in the order they were added. Unfortunately HTML hates you and won’t let you do that. —Jake Archibald, Deep dive into the murky waters of script loading
So, now our App.js is pretty barebones. It just needs to bootstrap our app, load up any additional features, and then pass control over to our router.
App.js
themeListener()
searchListener()
updateCart()
let urlParams = new URLSearchParams(location.search)
if (urlParams.has("search")) {
urlSearchHandler()
} else {
Router(location.pathname)
}
But, where are those functions coming from… Oh, no…
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title>Tell me this isn't a SPA</title>
<script src="/pages/Nope.js" defer></script>
<script src="/pages/ProductPage.js" defer></script>
<script src="/pages/Home.js" defer></script>
<script src="/pages/About.js" defer></script>
<script src="/pages/Products.js" defer></script>
<script src="/pages/Cart.js" defer></script>
<script src="/components/store.js" defer></script>
<script src="/components/Routes.js" defer></script>
<script src="/components/Router.js" defer></script>
<script src="/components/Product.js" defer></script>
<script src="/components/Link.js" defer></script>
<script src="/components/render.js" defer></script>
<script src="/features/search.js" defer></script>
<script src="/features/theme.js" defer></script>
<script src="/features/cart.js" defer></script>
<script src="/App.js" defer></script>
</head>
</html>
Play close attention to the order of these scripts. Hopefully, our folder structure makes a bit more sense now. The view templates are mostly just strings of HTML and some event listeners. They don’t actually do very much, but the strings they produce are essential for our app to function. So, they have to come first.
The components are the functions that actually do the work. So, they have to come next— followed by the features that rely on them. Finally, we have our App.js file which is the entry point for our application. It’s the first thing that runs when our page loads, but it needs all of the other files to be loaded first.
2-house-of-cards
Single File Components
We all like separation of concerns, right? This has kind of been a fundamental tenet of building apps on the web ever since like the PHP-pocalypse of 1999 where we were putting
mysql_fetch_row
in the middle of our table rendering. But, let’s talk about what Separation of Concerns actually means. We’re talking about reducing coupling and increasing cohesion. — Pete Hunt, Rethinking Best Practices
I want to take a brief aside and discuss what separation of concerns means in web development today. Ever since React popularized the concept of components and introduced JSX, the web development world has been moving away from separating our code by language. Instead, we separate our code by feature.
This has led to the rise of single file components in which we put all of our HTML, CSS, and JavaScript for each component together in one file. This way, we can easily edit each piece of the website as a whole without having to jump between multiple files. This also makes it easier to move components around and reuse them in different projects. Frameworks such as Vue, Svelte, and Astro have fully embraced this approach.
While CSS-in-JS has fallen out of favor, libraries like TailwindCSS continue to grow in popularity. Tailwind allows us to write our CSS in our HTML files using utility classes. This makes it easier to see what styles are being applied to each element without having to refer to a stylesheet. When combined with JSX, the entire behavior and appearance of a component can be seen all at once.
This is the approach that we will be taking in future articles, but I think it’s important to see how we got here. While our CSS file is only around seventy lines of code and it’s easy to find everything for now, it gets pretty intense by the end of this series. I don’t focus on CSS scoping in detail, but I may discuss it more in a future article if there is interest.
Conclusion
So, now our code is much more nicely organized. But, we’ve built a bit of a house of cards. All it takes to break the entire app is moving one file— or even one line of code containing the wrong global variable. Luckily, we’ve built everything here ourselves. If we were using multiple third-party libraries, it would be really difficult to know what order to load them in. What if one depends on the other?
And, we’re loading the entire app as soon as the first page loads. Although we have split the code into individual files, we’re still sending every single one to the browser no matter what page we’re on. Even with a small app like this, that’s a lot of unnecessary code.
Just imagine if we were loading in a bunch of third-party libraries. If just one route depends on a large library— like the events page that we will be adding in a few chapters— it slows down the entire app. Why should we have to load that code if we’re not even going to use it?
So, hopefully you can see why the web development world naturally drifted towards modules and bundlers. We will be discussing these in more detail as we progress through this series. First up— ECMAScript Modules.
Additional Resources
-
Fun Fun Function, Separation of concerns RANT
-
Daniel Schulz, In Defense of the Separation of Concerns
-
Jon Dewitt, Modern-Day Separation of Concerns