Evaluating the Optimal Javascript Configuration for Products and Large Projects
By Paul Suda
A major 2018 project for the Civis team is a ground-up Platform UI rewrite using modern Javascript tools. We spent a lot of time evaluating packages and tools as a part of this process, and here I’ll share what we determined is the best Javascript project configuration for a large, feature-rich product UI.
How we pick packages
Picking the right framework or library is complicated — the Node.js package ecosystem sees heavy churn, so there are a number of choices and it isn’t clear which will stick around long enough to match the lifespan of the project.
Our package evaluation criteria included: strength of documentation; how recently it was released; how familiar our engineers were with the packages (most have written unit tests with Jasmine and developed apps with React); a thriving ecosystem of associated NPM packages; and number of downloads.
The choice of toolkits for app development was easy because we’ve already done a number of React- and Redux-based projects in the past. We like React because it’s simple, has a healthy ecosystem of packages, and good developer tooling.
Localization
Our Platform roadmap includes expansion into multiple languages and date formats. While lots of frameworks (such as Ruby on Rails) come with a set of tools for handling translations, and date and number formatting, the NPM package ecosystem’s “roll your own” approach meant we needed to research and experiment.
We chose packages with the best balance between package size and ability to support locales with the largest potential user base. We evaluated Intl.js polyfill, which had much wider support for more locales, but added too much extra code and data to the build to be practical. We settled on Numeral.js to format numeric values, and we chose Moment.js for time and date formatting.
There are numerous language translation NPM packages available, but many haven’t been updated for over a year, or have extremely small download counts. We selected Polyglot.js because it is under active development, is supported by a major company (Airbnb), was well documented, and had a strong UI for both translating copy text and maintaining translations.
Since we pieced together our localization solution from three different packages and had many components that would use all three, we wanted to load them all at once for efficiency. To do this, we created a short file in lib/locale.js such as:
We could then easily load number formatting, date formatting, and translation helpers in our components with one line, similar to:
Managing CSS
Almost everything we’ve created at Civis uses Bootstrap as a base component library. On past projects, we’ve written a lot of custom CSS code, and in some cases, we spent more effort undoing Bootstrap styling rather than using it to achieve the final result our designers envisioned. Custom CSS code is difficult to maintain. In particular we’ve had difficulties with code duplication when different developers style different components in similar ways. Defining what blocks and elements should be styled and which should inherit styles has been difficult to do in a consistent way.
So, we went into this project with the mantra “write as little CSS as possible.”
This project conveniently coincided with our design group’s effort to create a design system, so we recommended design patterns and terminology more closely matching what exists in Bootstrap. This made it easy for us to take the brand colors, fonts, and other design tokens and plug them into Bootstrap to create a theme for the UI.
We significantly cut down the amount of custom CSS code, and using Bootstrap’s theming system allowed us to white label Platform for other brands and offer an alternate dark background version of our app.
Developer Tools
We started by defining a demanding set of coding standards, and agreed to adjust if they were slowing us down too much. As it turned out, we’ve dialed back only a few minor linter rules and are still at 100% code coverage with our unit tests. Like localization, setting up good developer tools is much harder later on in a project’s life. Starting upfront allowed us to stick to our schedule and kept our expected velocity, while creating better code with more assurance from tests that it works as intended.
For linting our code, ESLint was the obvious choice because of its popularity and support for React features like JSX, and we use the Airbnb style guide with a few modifications. We also created our own custom rule to hunt down places where text copy that should be translated appears directly in templates and strings.
To run unit tests and reports, we use Karma and Jasmine. Jasmine is nice because it comes with reporters, matchers, method spies, and defines specs with familiar rspec-like “it()” and “describe()” methods. Karma runs tests easily in a headless browser in CI and in desktop Chrome for local development.
This was the first Javascript project where we were aiming for 100% test coverage, so we set up Istanbul to help us enforce that. For segments of code where testing just doesn’t make sense, we can ignore coverage by adding a comment on the line above “/* istanbul ignore next */ .”
Istanbul instruments code using a plug-in for Babel, which adds a lot of additional code to the generated asset bundle. For bundle size and performance, it’s important to avoid instrumenting production code. A sample from our webpack.config.js that handles this:
We decided to try Flow, a tool that adds type checking (in a limited way) to Javascript and is known to work well with React apps. It’s a concise way to express what we’ve done in past projects with React’s PropTypes. We like not having to import PropTypes from ‘prop-types’ in every component, and the notation it uses to put the types in the class definition is cleaner.
An example of how we use Flow:
NPM tasks
For practical reasons, we needed to run our tests in a few different modes. When running tests in watch mode, we needed files to save quickly, but didn’t need coverage reporting. When running tests in our CI environment, we wanted to enforce coverage and require passing the linter as well.
Capturing the command line to start tasks like dev environment as NPM tasks helps onboard new developers onto the project, and provides an easy-to-find reference for the recommended process.
When working on code locally, we can run npm run unit-test-watch to continually unit test our code as we change it. We have npm run lint and npm run unit-test tasks to lint and test non-interactively, and an npm test task that we run in CircleCI on each pull request to make sure it builds.
Do it right from the start
To make sure all everything worked properly, the very first thing we created was a “hello world” page, which simply displayed the text “hello world” in a minimal layout. But, it did so with proper localization, was fully translated, fully tested with 100% coverage, compliant with our coding standards, and used well-formed React components.
Creating the simplest possible app with all of the dev tools in place helped us focus on getting the basics right from the start. We then began the real work of implementing navigation and other features.
Time saving or time consuming?
Doing the extra work to support everything at the start of the project ultimately saved us time. Sure, our first sprint was spent implementing a deluxe version of “hello world.” But, velocity following that sprint was much improved and spending a sprint focusing on proper project setup helped us form good habits from the start.
It is worth noting that this approach might not be right for all projects. A week or so of work spent on setup might not pay off for a project that only takes three weeks to complete. But, for a project with a longer timeline, the slow start allowed for greater velocity and more maintainable code later.