I Tried Creating An SPA With JavaScript and HTML 🤯
Tags: javascript, webdev, spa
Published: 2023-08-07 11:38:46

Due to the popularity of several modern frontend frameworks, many frontend developers are gradually losing touch of what it's like to build an entire application without using any framework or library. 
Based on <a href="https://dev.to/saje/create-reusable-html-components-using-only-javascript-1icc/" target="_blank" rel="noopener">my previous article</a>, I decided to try out what it would be like to create a Single Page Application (SPA) using only JavaScript and custom elements. So I ended up creating a "mini" ecommerce store. 
Even though I couldn't complete it, it provided a few insights which I found interesting. 

Before we continue, you can <a href="https://sagekyle.github.io/javascript-spa/" target="_blank" rel="noopener noreferrer">check out the web page here</a> or check out <a href="https://github.com/SageKyle/javascript-spa target="_blank" rel="noopener noreferrer">the repo here</a>.

> A <a href="https://en.wikipedia.org/wiki/Single-page_application" target="_blank" rel="noopener noreferrer">single-page application</a> (SPA) is a web application or website that interacts with the user by dynamically rewriting the current web page with new data from the web server, instead of the default method of a web browser loading entire new pages.

One popular concept in SPAs is _<a href="https://www.geeksforgeeks.org/what-are-some-advantages-of-component-driven-development/" target="_blank" rel="noopener noreferrer">Component Driven Development</a>_, which is basically an approach of separating your code into smaller reusable components that deliver a single/simple functionality.
After publishing <a href="https://dev.to/saje/create-reusable-html-components-using-only-javascript-1icc/" target="_blank" rel="noopener noreferrer">an article on custom elements</a>, which provided a way to create reusable components using vanilla JavaScript; out of curiosity, I've been thinking and enquiring how far we can push custom elements, or more specifically, how much can we do in JavaScript without our favorite frameworks/libraries, perhaps using just custom elements? When I asked this on social media, some of the reactions were quite funny but they sparked some interest in me. I decided to sacrifice some time to _choose stress_ (as one person described it), and find out how much stress it could cause. Turns out, it is quite a lot of stress - but not as much as I had imagined.
There are three major concepts that distinguish an SPA:
- **Structure:** by structure, I mean, the folder/code structure, the layout, components, etc.
- **Routing:** this, of course is very crucial since routing is handled in the frontend
- **State management:** even though this is not unique to SPAs alone, state management is very crucial in an SPA

Based on these three concepts, I've split this article into three sections, to discuss each of these topics based on how I approached them in my little adventure.

## Folder Structure
This aspect was perhaps the simplest aspect of the project - thanks to custom elements. As I explained in the article on custom elements, you can easily create reusable html components with JavaScript. So, as you might expect, I made a deliberate effort to make the folder structure look similar to what you'd have in a React application. Here's a screenshot of the expanded folder structure:


![a screenshot of all the files and folders of the spa](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zmegcitytfuruv0s5tr6.png)

Yes, quite a lot of files, I know - some of them aren't even being used at the moment - but this was a somewhat sarcastic mimicry of an SPA folder structure 😅
And if you're curious how the snippet of the components looks, here's the `navbar`:

![a screenshot of the navbar component of the spa](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/campknzu8ezun41fc9wh.png)

## Routing
Basic routing in an SPA is quite straightforward and relatively easy to implement. The way <a href="https://medium.com/geekculture/under-the-hood-react-router-466365fee324#:~:text=We%20use%20useState%20and%20useEffect,has%20some%20features%20to%20mention." target="_blank" rel="noopener noreferrer">React Router works under the hood</a>, for example, is by generating a `routes` object which contains all the defined routes, and then throw a `404` error when a user tries to access a route that is not among the predefined routes. 
To mimic this, I just created a `routes` object that looks like this:

```js
// routes.js
// all available routes
const routes = {
    '?about': () => {
		return about()
	},

    '?home': function () {
		return product()
	},

    '': async () => {
		return await home('')
	},
    404: () => {
		error404()
	},
}

export default routes
```

_It looks a bit messy I know, but there's a reason for that..._ 

In the snippet above, I created an object with the `route` as the key, and a function which returns another function as the value. The reason for using <a href="https://www.freecodecamp.org/news/higher-order-functions-in-javascript-explained/#:~:text=A%20higher%20order%20function%20is%20a%20function%20that%20takes%20one,functions%20like%20map%20and%20reduce." target="_blank" rel="noopener noreferrer">Higher Order Functions</a> is because I wanted to be able to pass arguments to the template function (to create 'dynamic' routes), and this seemed like the easiest approach.
Notice that I used <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/searchParams" target="_blank" rel="noopener noreferrer">`search params`</a> (`?`), instead of regular routing (`/`) - more on this soon. 

```js
// app.js
// import the routes object
import routes from './api/routes.js'
// Main container
const main = document.querySelector('.root')

// switch route without reloading the page (default behavior of links)
function handleRoute(event) {
	event = event || window.event
	event.preventDefault()
	window.history.pushState({}, '', event.target.href)

	handleLocation()
}
// check if the current path (url) is part of the preconfigured routes, else display the 404 page
async function handleLocation() {
	const path = window.location.search
	const templateFunc = routes[path] || routes[404]
	const html = await templateFunc()

	main.innerHTML = html
}
// make handleRoute() a method on the window object
window.handleRoute = handleRoute
// when user clicks back/forward, trigger the location function
window.onpopstate = handleLocation
// call the location function on first page load
handleLocation()
// export the function so it can be used within custom elements
export { handleRoute }

```

The comments in the snippets above are self-explanatory; first we import the `routes` object, then create a `handleRoute` function which we'll add to our links for routing across pages. The function takes an `event` argument (which is triggered when a user clicks a link), prevents the default behavior of the event (stop the page from reloading), and calls <a href="https://developer.mozilla.org/en-US/docs/Web/API/History/pushState" target="_blank" rel="noopener noreferrer">`history.pushstate`</a> to 'push' the user to the URL specified in the link, after which we call the function: `handleLocation`.
`handlelocation` is for getting the current URL the user is on (in this case the search params - which was what we used in the routes object) using `window.location.search`, check if that route is in the `routes` object. If it is, we return the contents of the route, else we return the `404` route, and then finally call the function returned by the `route` and append its content to the main page wrapper.
After configuring our routes, we can now create our "soft links" to link different pages without reloading the browser:

```html
<button class="nav-btn"><a href="/" onclick="handleRoute()">Home</a></button>
```

### Different ways of doing the same thing
There are different ways to implement routing in an SPA. Let's look at three of these:
1. **Regular Routes (/):** This is the standard approach often used in routing libraries, like React Router, etc. This method allows you to create routes that appear as separate pages, using forward slashes (/). To use this method, we'd need to modify the `handleLocation` function to use `window.location.pathname` instead of `window.location.search`, 

```js
// app.js
// check if the current path (url) is part of the preconfigured routes, else display the 404 page
async function handleLocation() {
    const path = window.location.pathname
    const templateFunc = routes[path] || routes[404]
    const html = await templateFunc()

    main.innerHTML = html
}
```

Then modify the routes object:

```js
// routes.js
// all available routes
const routes = {
    '/about': () => {
		return about()
	},

    '/home': function () {
		return product()
	},

    '/': async () => {
		return await home('')
	},
    404: () => {
		error404()
	},
}

export default routes
```

However, when using this method, you also need to configure your server to return a single HTML page for all routes entered by the user. Without doing this (since the HTML is always parsed before the JavaScript), the browser throws an error when it tries to access an HTML file for a route that doesn't match an existing HTML file. Of course if you're interested, you can easily use <a href="https://expressjs.com/en/guide/routing.html" target="_blank" rel="noopener noreferrer">Express</a> or any similar tool to implement this.
2. **Query Params:** This is the method I used above so I think you already get the idea. The main advantage of this (which made me choose it) of course, is that the browser already considers a URL with a query parameter as part of the base URL, so everything works "as a single page" without requiring any server-side configuration.
3. **Hash URL:** This - as you'd expect, is similar to using query parameters. The only difference is that it uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event" target="_blank" rel="noopener noreferrer">hashchange event</a> instate of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event" target="_blank" rel="noopener noreferrer">popstate event</a> used with query parameters. You can check out this video for more on this:

<iframe width="560" height="315" src="https://www.youtube.com/embed/JmSb1VFoP7w?start=1537" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

### What about dynamic routes?
The first approach that crossed my mind for creating dynamic routes was to create a function that accepts a `path` and `template` (in this case, a function that returns a template), and appends it to the `routes` object.

```js

// programmatically create a new route and append it to the routes object
async function createRoute(path, template) {
	routes[path] = await template
}

```

Unfortunately this didn't work, of which I can't yet fully visualize why (perhaps you could help).
Because of this, I had to proceed with my initial idea of passing `props` to the template function for the route. The complete `route` object ended up looking like this:

```js
// route.js
// all available routes
const routes = {
	'?about': () => {
		return about()
	},
	'?home': function () {
		return product()
	},
	'': async () => {
		return await home('')
	},
	'?category=jewelery': async () => {
		return await home('jewelery')
	},
	'?category=electronics': async () => {
		return await home('electronics')
	},
	'?category=men%27s%20clothing': async () => {
		return await home("men's clothing")
	},
	'?category=women%27s%20clothing': async () => {
		return await home("women's clothing")
	},
	404: () => {
		error404()
	},
}
```

> **Note:** I used <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI" target="_blank" rel="noopener noreferrer">encodeURI</a> to format the `prop` before appending it to endpoint for fetching the products.

## State Management
State management is arguably the most crucial, mind-boggling and perhaps most difficult aspect of frontend development today - especially in an SPA; this difficulty is escalated when you try to implement the same functionality without using any specialized tool (a state management library/package).
While it's relatively easy to fire events with immediate side-effects within a component, sharing and maintaining state in real-time across components is almost a nightmare if you're trying to re-invent the wheel. For example, my initial plan for the products category was to create a single route and then fetch and display the content based on selected category. To achieve this, I added an event listener in the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#using_the_lifecycle_callbacks" target="_blank" rel="noopener noreferrer">`connectedCallback`</a> method of the custom element to fetch and display the products based on a selected category, and then use recursion to re-render the component in order to reflect the state change:

```js
// home.js
// event listener to render a selected product category
connectedCallback() {
    this.shadowRoot.querySelectorAll('.category')
	.forEach((navLink) => navLink.addEventListener('click', async (e) => {
    let selectedCategory = e.target.textContent
    // update the selected product category
    category = await fetchProductCategory(selectedCategory)
    // call the home function to cause a re-render
    home()
}))
}
```

However, a weird behavior which I noticed was that, even the component was re-rendered, it still maintained the same stale data - basically, showing the default category instead of the new one. This led to the _messy_ nature of the `routes` object as stated earlier.
Admittedly, there might be other better alternatives or methods to effectively manage state in a Single Page Application using plain old JavaScript, but I'm yet to come across any. In my research, I came across several speculative implementation of the `useState` hook like <a href="https://rajatexplains.com/usestate-hook-behind-the-hood" target="_blank" rel="noopener noreferrer">this one</a>, but none of them was able to solve the challenge; and so with a little hesitation, I gave up.

## Conclusion
Yes, it is technically possible to create single-page applications using web components. However, one of the foundational issues with this method —which inherently applies to all SPAs— is the amount of JavaScript shipped to the browser. The total unminified size of JavaScript in the above project is about 2MB, which is quite A LOT of JavaScript for such a small app; of course, using a module bundler would significantly reduce this size, however, this doesn't override the performance impact experienced by the user on initial load.

The purpose of this little challenge —and even this article— is not necessarily to encourage developers to stop using frameworks or libraries, the main reason why these technologies were invented in the first place was to solve different challenges in the software development process; so, most of them are definitely useful and important. However, my perception is that developers should be curious enough to truly understand how these tools work and the tradeoffs involved when using them.
I hope this was a fun read for you, and don't forget to check out the main article before this on [custom elements](https://dev.to/saje/create-reusable-html-components-using-only-javascript-1icc/).