All Stacked Up: A Case Study

393

For all those who have followed our series so far, we want to thank you for your time, and we’re glad you can join us for this last installment! For those coming to this article on its own, we encourage you to go back and read the rest of our series, starting with our first post. In this final article, we will take a tour through a working case study created by Kenzan. The project leverages Yarn, webpack™, and TypeScript in not one but two GitHub repositories that anyone can clone, run, tinker with, and contribute back to.

Meet the Repos

For this case study, we will demonstrate the stack with React, while the other demonstration stack uses Angular:

Why Create Two Repositories?

Great question! There are a few principal objectives we hope to showcase throughout this case study, which will help to address why we went with two repositories.

  • This stack is intended to be flexible and interoperate easily with any modern JavaScript library or framework. Providing two versions that meet similar goals provides a better apples-to-apples comparison.

  • In line with the motivations behind TodoMVC, each repository has a reference app of equal feature parity.

  • With this case study, we can get down to a lower level on the implementation details, which is something we wanted to be mindful of during the other articles.

  • This article and the accompanying repositories aim to demonstrate the stack in an intuitive and straightforward approach. This will help you understand why a file is there and why a configuration option is what it is.

  • We chose to eschew using any sort of generators or CLI tools (like Yeoman, Angular CLI, or Create React App) so we can showcase just the technologies themselves as they are, at their least abstract.

Why Not Just Keep It Simple?

It does feel right to address the absence of a “vanilla” stack example. The simple answer is that, at Kenzan and in the industry as a whole, there are few scenarios where a major project would start without the use of some library or framework and all the nuances that come into play with that. For example, while AngularJS has the concept of modules and dependency injection, the developer still has to ensure correct load order of all JavaScript files, or else risk seeing $injector errors in the browser console.

The goal of this case study is to help readers understand the basics. It’s also to provide exposure to real-world scenarios that factor in the unanticipated complexity that often comes with welding multiple technologies together. These complexities aren’t always immediately obvious from the outside looking in. Our hope is to identify these technical distinctions so that you can better understand what is specific to React or Angular, and what is common to any basic implementation of the stack.

Ultimately, Kenzan believes that Developer Experience is a critical component in successfully delivering an application. We want to balance that fine line between prescriptiveness and flexibility, and allow teams to decide on how to cover the “last mile” of the stack—be it with Angular, React, Vue, or something of your own creation. With that, let’s dive into our stack!

QQl9XxQUpJC7enyK7IK1N2Mz94DwBxLAaNKnJo0j

Exercise – Get Ready, Fork, Clone!

Before you continue, we recommend that you fork and clone the React repository, and install dependencies using Yarn. That way you can follow along as we go through the repository in detail. There will also be a couple of exercises that we encourage you to try.

1. You will need to have the correct version of node and Yarn on your system. For details, see the “Project Setup” section in the README:

https://github.com/kenzanlabs/react-webpack-stack#project-setup

2. Fork the React repo:

https://github.com/kenzanlabs/react-webpack-stack

3. Clone your fork of the repo.

4. Open a terminal window and change to the root directory of the cloned repo. Then, install dependencies:

yarn install

Need some help? See the GitHub documentation on forking and cloning a repository.

Repository Overview

Starting at the root of our project, let’s review some key files and configuration patterns we’ve adopted.

Documentation

Every project should always have a robust README to ensure a good onboarding experience, and to act as a general reference for project practices and decisions. Important sections to include are an Overview, Installation/Setup, Usage (if it’s a library), Development Workflows, and Release Management. Other sections can be added, or links to external documentation can be used as needed.

For projects hosted on GitHub, there are helpful features including templates for issues and pull requests (PRs) as well as for contributing guidelines. You can find them in the .github/ directory of the project. If you are interested to learn more about GitHub best practices, check out Kenzan’s blog post on managing an open source project in GitHub.

Configuration and Code Quality

All configuration files for all tools used in the project are kept at the root of the project. These range from enforcing consistent code quality standards and best practices, to configuring our build tools like webpack or Karma.

  • .babelrc – Configuration for Babel when using Jest to run unit tests, and for transpiling our ES2015-generated TypeScript. This lets us leverage great plugins from the Babel ecosystem, like babel-preset-env, which provides more fine-grained control of the output of our JavaScript, and which pulls in plugins and polyfills as needed. (Imagine Can I Use as an npm package.)

  • .editorconfig – Rules for EditorConfig, a popular IDE-based plugin for high-level formatting rules.

  • .eslintrc – Linting and style rules for ESLint®. A consistent style guide is critical for a project. While this project primarily relies on TSLint, we still want to ensure consistency in any JavaScript files in the project (like our webpack configuration files and unit tests). When it comes to linting, whatever the opinions of the team are, the most important thing is to pick a definitive rule set and enforce that throughout the entire codebase.

  • jest.config.js / karma.conf.js – Configuration files for Jest (React) and Karma (Angular) respectively. Unit testing is a critical part of application development at Kenzan. We chose Jest and Karma for their deep understanding of the ecosystems they support, as they were created by the respective projects they are most associated with. For more on our principles and practices around unit testing, check out this video on test-driven development from a recent Kenzan-hosted Meetup.  It should be noted there is no reason one could use Jest to test Angular, or Karma to test React.

  • tsconfig.json / tslint.json – Configuration files for TypeScript (one for the TypeScript compiler and one for TSLint). We’ll cover these more in depth in the TypeScript section of this article.

  • webpack.config.*.js – webpack configuration files for the project. These are divided into three parts for composability across our different development lifecycles (more on this later).

  • yarn.lock – The auto-generated lock file for Yarn. The lock file should always be committed to the repository when there are dependency changes to the project’s package.json.

Project Layout

A designated src/ directory contains all of the project’s source code. The directory is organized into general categories, following a component-based paradigm.

  • index.js – The main entry point into the project. This acts as the application’s bootstrapping “glue” mechanism to kick off the app.

  • vendor.js – Third party npm dependencies. These let us configure wepack and provide an additional entry point.

  • components/ – The application’s components. Component-driven development is a mature development practice and is strongly advocated by all modern frameworks. Common examples are header, footer, navigation, contacts-list, and so on.

  • services/ – The application’s services. Generally, a service is a class that handles data or connects to a backend, and that does not interact with the DOM in any meaningful way.

  • views/ – Interchangeable with pages or screens. These are (container) components mapped to routes in an application, and serve as the shell or wrapper for a tree of components. Common examples are a Home or About page. The main goal is to support easy composition of components.

Development Workflows

The installation steps in the README only tell a developer to install Yarn and run yarn install. Other than that, all you know is that the project uses webpack and TypeScript. So how do you actually start working on the project? The README must have forgotten to document how to install the rest of the stack, right? Let’s take a look at a project’s package.json, specifically the scripts section.

What we see are commands you can run with the Yarn CLI, using yarn run. These command do the work of calling webpack or Jest, without us having to install any additional development tools. TypeScript, webpack, and the like come with CLI commands of their own. These become available on the command line locally to the project after running yarn install. We can see the entire list available in the node_modules/.bin/ directory. (The list will vary slightly repo to repo.)

K3atRyjvxvhrCrV0saTfVqx6blPTcPk9Iv_eD6bh

The entire project and all of its dependencies, whether for runtime or development, are portable and maintenance free. In addition, we chose to base our scripts on the concept of development “goals” or tasks (akin to Maven lifecycles). This provides a consistent methodology that can be applied to any project. Tasks are run by executing yarn run <script-name> or just yarn <script-name> as run is assumed.

  • develop – Used for development. This task leverages our “develop” webpack configuration (webpack.config.develop.js) and starts webpack-dev-server.

  • build – Runs a production build using webpack and our “prod” webpack configuration (webpack.config.prod.js).

  • lint – Convenience task for linting all files in the application.

  • test Runs unit tests, both locally and as part of a CI build process.

  • serve – Runs a production webpack build and serves it up locally. It is important to be able to easily run a production build locally, for example, to tackle a production-only bug.

What is important here is that our scripts are focused and deliberate. They are intended to be named intuitively and in a way that can be applied to any project. One nice feature about this setup is that the developer doesn’t have to know about the tools being used under the hood. If the team decides to choose a different testing framework, the script and dependencies just need to be updated. As far as anyone else is concerned, yarn test will continue to just work.

Continuous Integration

Let’s briefly talk about Continuous Integration. If you look in the bin/ folder, you will see a build script called build.sh. For all our projects at Kenzan, being able to run a build against any change to the repository is critical to ensure that building the project from scratch is reliable and stable.  This includes installing dependencies, running tests, linting, and of course a production webpack build.

At Kenzan, we use Jenkins with our clients to automate the building and deployment of PRs and projects. For our open source projects, like these two repositories, we have chosen to use CircleCI instead. That’s because it is a hosted and managed CI platform that we can share with all contributors. You can see the configuration in the .circleci/config.yml file in the repository.

Whatever CI tool is used, it’s important to have a consistent Continuous Integration server or environment where we can execute a versioned build script (build.sh). The build script runs each time a PR is opened or updated. Automation around deployments, while out of scope here, is a critical part of any infrastructure for our project. That’s why we felt it was important to showcase at least the “CI” in “CI/CD”, as a reflection of our best practices and standards.

webpack

For webpack, the main point to review is our configuration composition, which follows the recommendations of the webpack advanced usage documentation. During the discussion about configuration, code quality, and npm scripts, we mentioned that there are three configuration files: webpack.config.develop.js, webpack.config.prod.js, and a shared config called webpack.config.common.js. Let’s start with the common config.

webpack Config – Common

Creating a common configuration file lets us create additional composable and focused webpack configurations to support various goals, like develop and build. The common configuration is principally responsible for the following concerns.

  • entry / output – The point from where webpack will start building the dependency graph of our application, and the location of the build output.

  • rules (loaders) – File processing for our application that will handle our styles, images, TypeScript, HTML, and anything else that is required to build our application from source code and vendor dependencies.

  • plugins – Shared post compilation for:

    • Injecting our script and style paths into index.html using HtmlWebpackPlugin.
    • Configuring code splitting using CommonsChunkPlugin. In this case, we are creating a common chunk, unsurprisingly called “common”, which includes the common code from our entry points. This can be part of an effective caching strategy.
    • Exposing non-modular vendor libraries (like jQuery® references in Bootstrap) to webpack using the ProvidePlugin. The left side is the global reference as found in the vendor code. The right side tells webpack to map that reference to the equivalent of a require(‘jquery’) command.

Note: For React with HMR support, we have added react-hot-loader to our TypeScript loader configuration

Note: For Angular, we needed to add the ContextReplacementPlugin to resolve a warning emitted by Angular. We also disabled minification of HTML in our rule configuration based on an issue with html-loader that we ran into.

We will now look at how we can re-use this configuration using webpack-merge to support our develop and production workflows.

webpack Config – Develop

Using webpack-merge, we can compose our own webpack configuration together, to reduce duplication and bloat. If we examine the webpack.config.develop.js file, we can see there’s actually very little to it. We require our common configuration and pass that to webpack-merge along with an additional object just for our development-related webpack configuration.

  • output – For HMR, we need to configure an output file.

  • webpack-dev-server – Configuration for our local development server. Note that this is where we enabled the hot flag to enable HMR.

  • rules (loaders) – Special development-specific SCSS/CSS file processing. For development only, we have a loader configuration for SCSS/CSS that puts all our styles in a <style> tag. This is less intensive for development workflows.

  • plugins – Development-specific plugins:

    • Hot Module Replacement makes the magic happen. You’ll get to experience HMR yourself in the exercise below!

webpack Config – Prod

For our production builds, we will use webpack.config.prod.js. As with webpack.config.develop.js, we include our common configuration, and pass in an object along with additional production-specific configurations.

  • rules (loaders) – Used to generate a standalone .css file using ExtractTextPlugin, for better control over debugging, caching, and distribution of the application’s CSS.

  • plugins – Production-specific plugins:

    • webpackMd5Hash – Used to generate a [chunkhash] based on the contents of our files. This is an effective technique to implement as part of a caching strategy, as we can use the file contents, and whether they changed or not, to dynamically determine the file name generated by webpack. In this way, a chunk or file can cache bust itself only when needed. Pretty neat!

    • UglifyJSPlugin – When shipping an application to production, it is important to minify and strip out all excess whitespace and comments from our files, to help reduce the overall bundle size. This plugin does exactly as advertised!

    • ExtractTextPlugin – Used to generate a standalone .css file for better control over debugging, caching, and distribution of the application’s CSS (as opposed to the approach we used just for development purposes).

Exercise – Swap Modules with HMR

One of the great features we discussed in our webpack article is Hot Module Replacement (HMR). HMR lets you make changes to parts of a running application (such as changing HTML, JavaScript, or CSS) without having to reload the entire application, while it preserves any current state.

This is huge. Think back to our example of filling out a form, from our article on webpack. Imagine you want to test validation rules or submit/rejection functionality. Every change to the code in most build pipelines would reload the entire page, forcing the developer to tediously walk back through all the steps of filling out the form. Help us, HMR!

To demonstrate, let’s work through an example to see how HMR can save developers time during the development process.

Note: For now, given the complexities of Angular and our desire to keep the repos simple, you will only be able to use the React repo to complete this exercise. We’ll be doing a followup on HMR in Angular on the Kenzan Tech Blog, so stay tuned!

1. To begin, start the webpack development server (webpack-dev-server). Open a terminal window and change to the root directory of the cloned repo. Then run the develop task:

yarn develop

A browser window opens and loads the application’s home page. You’ll see a button to add a new contact.

2. Click Add contact to display a form for adding a new contact. There are fields for name, email, phone, and more.

3. Fill out part of the form with some user information, like your first and last name.

eMx5gfIWRkXhHL4nC5y32FciyZKI4_dn8nYeHmM-

Here’s where it gets fun. We’re going to try making some style changes. The background color of the page is a little on the plain side, so let’s change it. Normally we would expect this to reload our page and force us to fill out all the form fields again. But will it?

4. In a text editor, open src/components/bootstrap/bootstrap.scss and add the following CSS:

body {
background-color: #2aabd2;
}

5. Save the change to bootstrap.scss. When you do, the develop task automatically runs again. Look at that—the page changed color, but our form field entries were preserved. The state of our component remained unchanged!

yYAIUUIiInonodQHvLUG3q89bObU1VF7zfZx7EYu

That’s pretty cool. But you might be thinking there’s no way this will work if you change the actual ContactsListForm component. Let’s put that to the test.

6. In a text editor, open src/components/contacts-list-form/contacts-list-form.tsx (for React) or src/components/contacts-list-form/contacts-list-form.html (for Angular).

7. Let’s change the label for the First Name field to note that it’s required. Find the following text:

<label className=' control-label' htmlFor='firstname'>First Name</label>

And add (required) after the field label, like so:

<label className=' control-label' htmlFor='firstname'>First Name (required)</label>

JRIyJ4i0bF2WqOZCdqRicJ8AW7ZlKtpPmokddC-g

Hopefully this has helped demonstrate the power of HMR, as well as the kind of infrastructure and development platform webpack brings to the table.

TypeScript

As our previous article on TypeScript focused primarily on syntax, this walkthrough will focus on the configuration elements of TypeScript.

TSConfig (tsconfig.json)

TypeScript allows developers to use a JSON file to configure the TypeScript compiler. That config is automatically used when the compiler (tsc) is invoked (via command line, webpack loader, or the like). While we won’t go over every TypeScript compiler option available, let’s discuss a few options that are most meaningful to the setup of these projects.

  • noImplicitAny – This is set to true and requires everything (argument, return value, variable, and so on) in our application to require a type.

  • target – Specifies the version of the compiled JavaScript output we want from the TypeScript compiler. In our case, since we want to take advantage of Babel to further refine our generated JavaScript for production, we choose to have TypeScript output ES2015-compatible JavaScript.

  • module – We chose “commonjs” here since we have certain external npm packages that don’t support ECMAScript module syntax. Otherwise we would have chosen ES2015.

  • awesomeTypescriptLoaderOptions (useBabel) – Configuration specifically for the webpack loader awesome-typescript-loader. As the name implies, it really is awesome! We think so especially for its support of Babel, which is important to us because, as mentioned before, we think babel-preset-env is a powerful and versatile tool. We love the ability to write in TypeScript but transpile down to JavaScript that only polyfills the native browser features missing from our target browser demographic, delivering a much more fine-grained and slimmer bundle to our users.

Note: For our React repository, we enable the JSX flag to enable JSX support in the TypeScript compiler.

Note: For our Angular repository, we enabled the emitDecoratorMetadata and expirementalDecorators options since Angular leverages the ES2016 Decorators feature,

TSLint (tslint.json)

When it comes to linting and coding styles, every team and project has its preferences. At Kenzan, we understand those preferences are important and meaningful, and they they were picked for a reason. Our focus is not so much on what a team decides, but rather that the team makes these decisions binary (for example, the linter will either pass or fail on a given rule violation). This isn’t meant to be nitpicky but rather to ensure consistency, especially for code reviews.

One thing that we are excited to follow is a WIP (work-in-progress) plugin for ESLint that would extend to TypeScript files, allowing for consistent rules across all JavaScript projects.

Type Definitions (@types)

It is worth touching upon how TypeScript type definitions are managed. As discussed in our last article, Type Definitions are an important part of the TypeScript ecosystem. They allow non-TypeScript projects to play nicely within the TypeScript based ecosystem of your application. If you look in package.json, you will see a number of entries in the devDependencies section that start with @types/. This is the new workflow successor to using typings, available in TypeScript 2.0+. It allows developers to install definition files available from Definitely Typed using their package manager without any additional configuration. An example using Yarn would look like:

# we use the --dev flag since @types are only needed for development
$ yarn add @types/react --dev

Note: For those who may not be familiar with @ syntax, this is referred to as a scoped package.

Exercise – Try Out TypeScript

To demonstrate some of the features of TypeScript and how they improve the developer experience, let’s start making some changes and see what happens.

1. Open a terminal window and change to the root directory of the cloned repo. Then run the develop task:

yarn develop

A browser window opens and loads the application UI.

2. In a text editor, open src/views/home/home.tsx and find the following text:

{ContactService.buildName(contact)}

3. Also open src/services/contact.service.tsx and find our interface (ContactInterface) and our class (ContactService).

You’ll notice that buildName takes a single parameter, of type ContactInteface, which itself defines the properties and value types of a “contact” in the context of our application. In our case, a contact has properties like firstName, lastName, phone, and email. Some of these properties are optional, as denoted by a question mark (?).

4. Let’s make a change to the code in home.tsx to see TypeScript in action. Instead of passing a ContactInteface parameter, pass a string (like your name) instead:

{ContactService.buildName('Owen')}

5. Save the change to home.tsx. When you do, the develop task is automatically run again. You’ll immediately see an error in the terminal, indicating that we are calling buildName incorrectly.

AqquHcFFIBkunAwI9Iz9gyNM8ZhUPvobz8JAyckN

Luckily, most IDEs have TypeScript support, so a developer can get feedback that way as well. Here’s an example:

MuUZJiXynRQq8w1tiYCVyN9uULmR0qi5a7fir148

6. Let’s try another experiment. First, undo the change you made to home.tsx and save the file. (Notice in the terminal that it builds correctly now.)

7. Now let’s make a change to ContactInterface in contact.service.ts. We’ll remove firstName from the interface. Find the following line and delete it:

firstName: string;

8. Save the file and notice what happens in the terminal when the develop task automatically runs.

As we saw in the previous example, TypeScript can let us know if we are using part of our application incorrectly. This safety net is especially helpful in maintaining integrity in the entire application.

mJ0C7cIhvJBDvQ_WhxuRRwZhy59rot0RPHZ5GRGP

This time, we see two errors pop up in two different files. The errors show all the places where this change will cause an issue. This is helpful for refactoring, or just for catching an errant mistake by a developer at build time.

The type system of the application, in particular through the use of interfaces, helps developers ensure integrity throughout the codebase by providing core objects that can model our application domain and keep all components in sync. So we can see that, as a project grows and scales, TypeScript becomes more valuable at communicating the potential widespread impact of a given change.

As soon as you save the file, the develop task runs, and this time it should be happily error-free. Thanks, TypeScript!

Conclusion

We hope that this case study on our modern front-end stack has been valuable, and that it provided a good complement to our blog series. We strived to make sure the technologies used and the decisions made were as deliberate and intuitive as possible, so that the best practices we’re promoting are as accessible as possible. We hope you will contribute and ask questions through the issue trackers of our repositories. And if you have an improvement you would like to make, please feel free to submit a PR!

Kenzan is a software engineering and full service consulting firm that provides customized, end-to-end solutions that drive change through digital transformation. Combining leadership with technical expertise, Kenzan works with partners and clients to craft solutions that leverage cutting-edge technology, from ideation to development and delivery. Specializing in application and platform development, architecture consulting, and digital transformation, Kenzan empowers companies to put technology first.

Read the previous articles in the series:

A Modern Day Front-End Development Stack

Untangling Package Management in JavaScript Applications

Faster Tied Together: Bundling Your App with webpack

TypeScript: Our Type of JavaScript