Do we still need JavaScript frameworks?

Do we still need JavaScript frameworks?
As a web developer, I try to assess my toolbox regularly and determine if I can do without this or that tool. Recently, I have been investigating just how easy it is to develop a complex front end application without a front end framework.

As a web developer, I try to assess my toolbox regularly and determine if I can do without this or that tool. Recently, I have been investigating just how easy it is to develop a complex front end application without a front end framework.

What is a JavaScript framework?

Put simply, a JavaScript framework is a tool that you can leverage to develop advanced web applications, especially SPAs.

Back in the day, web developers would implement front end logic by relying heavily on vanilla JS and jQuery. But, as front end applications became more and more complex, the tools rose to meet that complexity.

This is image title

The frameworks that are popular today have a few core commonalities. Most front end frameworks/libraries, from Vue to React, provide some combination of the following:

  • Synchronization of state and view
  • Routing
  • A Template System
  • Reusable components

Are Frameworks still necessary?

It depends on how you stress the word necessary. Many would argue that front end frameworks are not and never have been necessary. They’re very useful tools, though.

So the question is: are frameworks today’s jQuery? Will the problems they solve be addressed by changes like those to the DOM API?

This is image title

It’s hard to say, but advances in native JS, the web component spec, and easily configurable build tools, have made developing a SPA without a framework as easy as it has ever been.

To examine this further, I developed a single page application using only vanilla JavaScript, native Web Components, and Parcel. There were a handful of pitfalls and difficulties along the way that highlighted the strengths of JS frameworks.

At the same time, once I passed the initial hurdles, I was surprised by how simple it was to create a single page application with just vanilla JS.

Overview

The application is simple. It is a recipes application with basic CRUD capabilities. The user can create, edit, delete, favorite, and filter a list of recipes.

This is image title

This is image title

Create Recipe Screen

Components

Creating a web component is also straightforward. You create a class that extends HTMLElement (or HTMLParagraphElement and so on), and then use that class to define a custom element.

You can also make use of lifecycle hooks such as connectedCallback, disconnectedCallback, attributeChangedCallback.

import template from './recipe.html'
import DATA_SERVICE from '../../utils/data'
export default class Recipe extends HTMLElement {
  constructor () {
    // attach shadow DOM, initialize private recipe property, and initialize data service
    super()
    this._shadowRoot = this.attachShadow({ mode: 'open' })
    this._recipe = null
    this.ds = new DATA_SERVICE()
  }
  connectedCallback () {
    // set html content to imported template
    this._shadowRoot.innerHTML = template
    // attach delete method to delete button
    this._shadowRoot
      .querySelector('.delete')
      .addEventListener('click', () => this._delete())
  }
  _render (title) {
    // set recipe title and text of favorite button
    this._shadowRoot.querySelector('.recipe-title').innerHTML = title
    this._shadowRoot.querySelector('.favorite').innerHTML = this._recipe
      .favorite
      ? 'Unfavorite'
      : 'Favorite'
  }
  _delete () {
    // delete recipe or display error
    try {
      await this.ds.deleteRecipe(this._recipe.id)
    } catch (e) {
      console.error(e)
      alert(
        'Sorry, there was a problem deleting the recipe. Please, try again.'
      )
    }
  }
  get recipe () {
    // getter for recipe
    return this._recipe
  }
  set recipe (recipe = {}) {
    // setter for recipe which triggers render method
    this._recipe = recipe
    this._render(this._recipe.title)
  }
}

window.customElements.define('recipe-item', Recipe)

recipe.js

My Recipe Item component for displaying recipes in a list

Routing

The routing for the recipes application is also quite simple. Given a navigation event, I set the application’s content to the corresponding web component.

Initially, I was using an npm package called Vanilla JS Router. With the browser history API, it isn’t all that complex to implement your own in less than 100 lines of code! Note: I am not implementing really complex logic such as route guards.

import './components/error/error'
import content404 from './components/404/404.html'
import DATA_SERVICE from './utils/data'
const ds = new DATA_SERVICE()
// get SPA containing element
const $el = document.getElementById('app')

// define routes
const home = async () => {
  await import('./components/recipe/recipe')
  await import('./components/recipe-list/recipe-list')
  await import('./components/modal/modal.js')
  $el.innerHTML = `<recipe-list></recipe-list>`
}

const create = async () => {
  await import('./components/create-recipe/create-recipe')
  $el.innerHTML = `<create-recipe></create-recipe>`
}

const edit = async () => {
  await import('./components/edit-recipe/edit-recipe')
  $el.innerHTML = `<edit-recipe></edit-recipe>`
}

const error404 = async () => {
  $el.innerHTML = content404
}

// match routes with paths
// grab recipe by id param for edit route
const routes = {
  '/': home,
  '/create': create,
  '/error': error404,
  '/edit': async function (params) {
    const id = params.get('id')
    const recipe = await ds.getRecipe(id)
    await edit()
    $el.querySelector('edit-recipe').recipe = recipe
  }
}

// on pop state get params from url and pass to route
// if no such route, error
window.onpopstate = async () => {
  const url = new URL(
    window.location.pathname + window.location.search,
    window.location.origin
  )
  if (routes[window.location.pathname]) {
    await routes[window.location.pathname](url.searchParams)
  } else routes['/error']()
}

// on pop state get params from url and pass to route
// if no such route, error
// add route to browser history
let onNavItemClick = async pathName => {
  const url = new URL(pathName, window.location.origin)
  const params = url.searchParams
  if (routes[url.pathname]) {
    window.history.pushState({}, pathName, window.location.origin + pathName)
    await routes[url.pathname](params)
  } else {
    window.history.pushState({}, '404', window.location.origin + '/404')
    routes['/error']()
  }
}

// on page load/reload, set appropriate route
;(async () => {
  const url = new URL(
    window.location.pathname + window.location.search,
    window.location.origin
  )
  if (routes[window.location.pathname]) {
    await routes[window.location.pathname](url.searchParams)
  } else routes['/error']()
})()

// export routes and nav click method
const router = {
  onNavItemClick,
  routes
}
export { router }

router.js

That is a quick summary. I want to keep this article to a reasonable length. I may write a follow-up post with a more thorough explanation of the application. I implemented some fun features like infinite scrolling, a custom drag and drop uploader, and more!

Retrospective

After creating the application, I took some time to think about the pros and cons of the whole process from start to finish. I’ll start with the bad news.

Cons

The spec is still in flux

This is image title

The web component spec is both old and new. It has been around for a lot longer than I had originally thought. Web Components were introduced by Alex Russell at Fronteers Conference 2011 for the first time. However, the push behind web components has really grown in the past year or two. As such, there is still a lot of turmoil in the spec. For instance, HTML imports have been abandoned, though most of the documentation/resources still reference them.

Testing

This is image title

There aren’t a lot of dedicated resources for testing native web components out there. There are some promising tools such as skatejs ssr and the web component tester from Polymer. But these tools are really meant for use with their respective libraries. This presents some difficulties for use with native web components.

Change Detection

Having an underlying system that automatically keeps the view in sync with the data model is incredible. It is what drew many to Angular and other frameworks in the first place.

Keeping state in sync with the view is not so difficult at a small scale. But it can get out of control very quickly, and you find yourself adding tons of event listeners and query selectors.

This is image title

The Shadow DOM

I am really torn about the shadow DOM. On one hand, I love the idea of encapsulation. It is a sensible design pattern, makes your style cascade more manageable, simplifies your concerns, and so on. However, it also presents problems when you do want certain things to penetrate that encapsulation (such as a shared style sheet), and there are ongoing debates about the best way of doing this.

Generating DOM structures

Part of the magnificence of frameworks/libraries like Angular and React is that they are masters of their DOMain. That is, they are excellent at efficiently rendering and re-rendering structures in the DOM. From the Angular University blog:

Angular is not generating HTML and then passing it to the browser to have it parsed, instead Angular is generating DOM data structures directly!

Angular for example, unlike jQuery, renders DOM data structures directly. That is, instead of passing HTML to the browser to be parsed, and then rendered into DOM data structures. This is more performant as it eliminates that parsing step. The Virtual DOM is also quite useful, as they prevent you from re-rendering everything each time you need to update your view.

Pros

On the other hand, there are some undeniable benefits to developing applications this way:

Bundle Size

The final product can be (emphasis on can) so much smaller and more compact than anything developed with a modern framework. For example, the final build of my fully featured recipes app was less than half the size of a fresh Angular build.

This is image title

This is image title

This is image title

Note: These are the updated, optimized bundle sizes.

Understanding

This is image title

If you’ve only really developed with a framework and its CLI, it can be a great exercise to make a web application without extra tools. As someone who wants to achieve a certain level of mastery (to the extent that it’s possible) of web development, it has been essential for me to get more hands-on experience with build tools, browser APIs, design patterns, etc.

Performance

This is image title

What these front end frameworks and libraries are doing behind the scenes is amazing. However, you can pay a performance price if you decide to use any of them; there is no such thing as a free lunch. There are many potential performance drags at scale: whether it’s wasted re-renders, redundant listeners, deep object comparison, or unnecessary and large DOM manipulations. You can cut out a lot of complexity here by implementing things from scratch.

The Angular and React teams seem aware of these pitfalls, and have provided things like shouldUpdate method overrides and onPush ChangeDetection as a means of further optimizing performance.

Simplicity and code ownership

This is image title

You take a risk whenever you bring in 3rd party code. This risk is reduced with tried and tested libraries/frameworks, but never truly eliminated. If you can get away with writing code yourself or with your team, you can reduce that risk and maintain a codebase that you know in and out.

Notes and interesting tidbits

I had a blast working with Parcel. It felt a bit more limited than Webpack at times when trying to work around certain edge cases, but I found the, ‘zero config’ tag line to hold true, for the most part.

It is also clear to me that many label React a ‘library’ and Vue a ‘progressive’ framework. While I understand the reasons for this, I think React, Vue and Angular solve many of the same problems. Thus, I am considering them all together under the term ‘frameworks.’

Why not use Stencil or Polymer? I wanted to avoid the use of packages, libraries, and frameworks as much as possible. I wanted to see how far web standards had risen to meet modern development (aside from build tools).

There are I’m sure many other ways of developing a SPA or front end application in general without a major framework or library, I tried one way here, and I’d love to hear about others!

Summary

A great heuristic for the decision to use or not use a framework is what I call, “the tipping point.” There comes a point as your application grows, where you end up creating your own framework in order to reuse functionality and structure. For example, you have a bunch of forms and you want to create reusable logic for reactive validation.

If you end up at this point, you have to decide whether or not it is worth investing the time in creating systems to accomplish what you can quickly accomplish with a framework or library. There will be different tipping points depending on what your time constraints or budget constraints are, but frameworks are still very relevant given the right scenarios.

That said, much of what frameworks do will probably become easier to do with smaller libraries and/or native code as time goes on. Take my application as an example. At the same time, if the large frameworks and libraries remain versatile they may morph, adapt, and stick around. If not, they may end up like jQuery — a tool of the past for the most part.

Conclusion

In conclusion, there are promising ways of developing complex front end applications without frameworks. However, the spec for things like web components is still evolving and there are kinks to be worked out. The frameworks still do a lot of amazing things and can make development much smoother.

At this time, as far as I can tell, the pros of using a framework often outweigh the cons. However, if frameworks do not start solving new problems and continuing to evolve, they will eventually fade away.

Suggest:

Web Development Tutorial - JavaScript, HTML, CSS

Javascript Project Tutorial: Budget App

JavaScript Programming Tutorial Full Course for Beginners

E-Commerce JavaScript Tutorial - Shopping Cart from Scratch

Learn JavaScript - Become a Zero to Hero

JavaScript for React Developers | Mosh