Modernizing StackShare's Front End: The React + Rails Stack

Introduction

StackShare, like many other startups, wasn’t built to scale. What started out as a simple Rails app with very few requirements, eventually grew to what you see today. When I joined StackShare in February of this year, we were just starting to scale the team (we’ve gone from 5 to 11 people since then!). Given all of the new shiny things we wanted to build and that there would need to be more collaboration on the front-end, I knew we needed to modernize our front-end stack as quickly as possible.


Application survey

We started by conducting a survey of the current state of the application. This exercise provided us with a list of problems to tackle. The main problems we faced included:

  • Unused library dependencies
  • Outdated dependencies
  • Multiple competing libraries (React + Angular + jQuery)
  • Code quality issues
  • Overuse of globals
  • No front-end tests
  • Improper use of certain libraries
  • Inadequate DIY solutions
  • No SSR (Server Side Rendering)
  • Little consistency across the codebase


Goals

All of these things could be addressed but not all at once, so we took a staged approach. Before we could begin making changes, we needed to define where we wanted to be. With this in mind, we made a non-exhaustive list of the highest priority goals:

  • A clear, team-wide, understanding of how a front-end app should be built
  • A test framework that enabled us to write effective and high-value tests
  • A linter to enforce code consistency and quality
  • CI tooling to make things easy and automatic
  • A standardized project folder structure & file conventions
  • Up-to-date dependencies (and no unused dependencies)
  • An isomorphic React app
  • A component library
  • A living style guide


Making progress

I’ll explain how we achieved each of the goals and thus addressed our major pain points. We’ll, of course, review a few of the awesome tools and libraries we used as well as some of the outcomes.


A clear, team-wide understanding of how a front-end app should be built

Each person on our team came from different backgrounds and experience levels. The existing application was built by engineers with mostly back-end experience. Moving forward, I knew we all needed to be on the same page.

I wrote up everything and put in our GitHub wiki, then shared it with the team. It’s important to share early and get team buy-in before making sweeping changes. The wiki content defined the roles and responsibilities of the front-end and how they fit into the broader product application.

During the planning phase, we referenced other guides and sites that describe how a modern web app should be built. If not for our Rails backend, we would have used create-react-app to scaffold our front end. When using Rails and React, there are some tricky things that need to be done with Webpack, such as ensuring the Rails views know how to create the mount points for the React components, coordinating serving Webpack assets, and bootstrapping the client.


A test framework that enables us to write effective and high-value tests

We didn’t have any front-end tests, so we needed to get a framework in place before building out new features. I’ve used many (if not all) of the JavaScript testing tools out there, but this time I went with Jest. It’s capable, fast, easy to use, provides snapshot testing, and has a great watch mode. We are three months in and have no regrets choosing Jest.

One of the big wins for our team was being able to do Jest snapshot testing. Snapshot tests capture the markup and CSS output from rendering a React component, saves it to a file, and then uses that to detect differences in subsequent test runs. We insist on having comprehensive snapshot testing to catch style and layout regressions. We also use glamorous, which works great with Jest snapshots to catch CSS regressions. Most of our unit testing uses Enzyme for shallow rendering, and this has worked well for us.


A linter to enforce code consistency and quality

This was a no-brainer for us, and we chose ESLint. We also configured some plugins for React, Prettier, and Babel. Part of this exercise was identifying all the globals that were being referenced in legacy JavaScript. These became “exempted” in the ESLint config, which doubles as a task list for us to work on in the future. We moved all our legacy JavaScript code into a bundle and gave it its own .eslintrc file. This way we didn't have to fix everything at once.

"rules": {
  "react/no-string-refs": "warn",
  "react/prop-types": "warn"
}

As we replace legacy code with new features, that bundle will shrink and eventually go away.

I think the one thing that has had the most impact, and immediately became a “non-issue” was the adoption of Prettier. Since it was baked into our CI process and was not really configurable; the team never once debated formatting or code style.

Linting along with Prettier lets us focus on the more important aspects of code reviews and thus saves us heaps of time.


CI tooling to make things easy and automatic

We use CircleCI to automatically run builds and tests whenever we push code to GitHub. The workflow feature allows us to run build tasks in parallel which really speeds up build time.


StackShare CircleCI Workflow


The first step is install-client which uses Yarn to gather dependencies and cache that layer for further steps. The next three steps are run in parallel. build-client uses Webpack to compile all the code. lint-client runs eslint and outputs JUnit format files. test-client runs the Jest tests and outputs LCOV format reports directly to Code Climate. Any errors or warnings are collated and presented in detail in the CircleCI UI and also at a pass/fail level on the GitHub PR. All of these commands can also be run in the terminal using yarn scripts, or directly in an IDE such as RubyMine/WebStorm.

This setup reduces human error, improves our velocity, and increases developer happiness.


A standardized project folder structure & file conventions

In the absence of any kind of consensus or convention, folder and file structure can quickly get messy. It’s important to get this right before your team scales up, so your repo has consistency and engineers can work efficiently. They work efficiently by knowing where to find things, where they should put things, and how they should be created/named/etc.

The exact structure and conventions should fit your team and application, and there are plenty of existing project templates out there. Here are some of the things to consider when designing your project layout:

  • How are files named? Title case, lisp-style, snake case?
  • Where do test files live? Next to the file-under-test or in a dedicated tests folder?
  • How are Webpack bundles organized?
  • Where do shared React components live? What is the folder naming conventions for common UI elements?
  • Where does shared code live?
  • Where do configuration files live?
  • How are your data-layer files organized? Ducks and Re-Ducks are popular for Redux projects and we’ve adapted these to suit our Apollo GraphQL setup.

We documented our choices in our GitHub wiki and continued through the process of refactoring the files into their new homes. Since then, our team rarely discusses where things should live or where to find things. We know to look in the shared library for a UI component before building it from scratch, and so on.


Up-to-date dependencies (and no unused dependencies)

Over time a JavaScript app tends to accumulate dependencies. But sooner or later you need to stop and clean house. It’s not a fun nor quick job, but it’s important; so make time for it in your planning. We went through the list of dependencies in our package.json file and used a combination of techniques to determine if we still needed it:

Project searches using keywords; looking for imports or references to the library API objects Removing the package in question and running tests or manual testing; this assumes you have a reasonable level of test coverage Asking the team “why do we have this package?”

Once the unused stuff was cleaned out, we moved onto upgrading packages. This is a bit more involved, especially when you get into breaking changes like going from react-router v2 to v4 or big jumps in React or Babel. We relied on end-to-end tests, manual testing, and reading change logs. It feels really good to have an up-to-date app.


Isomorphic React App

We were starting to build out features using only React components. Historically, our app was built using HAML views rendered by Rails, along with a good dose of jQuery to make things “happen” in the browser. We loved React but didn't want to sacrifice the performance of a server-rendered page or suffer the SEO consequences of client-rendering.

Since our site still runs on Rails, it wasn't as straightforward to implement isomorphic React as it would have been with Node.js. We decided on Webpacker to connect our HAML views to our Webpack bundles. To handle client rendering, server rendering, Apollo cache rehydration, and glamorous style rehydration, we built our own React rendering library. The server rendering part of it runs as a Node.js app which is deployed as a microservice. Our main app uses this microservice to render views using React via Rails view helper functions. It works really well and lets us fine-tune the scaling, caching and error handling.

React components should be props-driven so the server-side rendered output is determined by props. It needs to be deterministic so the rendered output won’t have conflicts when rehydrating the components in the browser. A good example of this is a “notice message” that is controlled via a flag in localStorage. Since this can’t be accessed on the server, we had to make the initial render not show this notice. The client then reads from localStorage in componentDidMount() and fades in the message. This sequence provides for a seamless UX regardless of whether SSR is enabled or not.

We also needed to ensure that views could quickly fail back to “client render mode” if the Node.js microservice wasn’t available. If the service fails, users can still use our site instead of seeing an error message or broken view. It also allows our engineers to easily test SSR in the local development environment by simply starting or stopping the Node.js SSR service.

We are hoping to open source this library after it has proven itself in production… stay tuned!


A component library

A well-designed component library is an invaluable shared asset for design and engineering teams. It benefits the overall product team by drastically increasing velocity and consistency. A component library, among other things, is an artifact of a mature design system. There is so much written about this topic so I won’t go into too much detail here, but feel free to research the subject on your own.

One of the first things I set up was a home for a shared component library and a mandate that all potentially reusable UI elements live in that home. When an engineer sits down to implement a UI, they first check the shared component library for existing elements. Then, they ask the team in the #front-end Slack channel to double-check something isn’t “in-flight”. Then they will disclose their intent to add a new component and follow up with details later on when it’s on our Storybook site.

We use (and love) Storybook for developing components. It lets us build components in isolation, design props, iterate quickly with teammates, and quickly progress a UI into production. All shared library components must have a comprehensive Storybook story with all the prop permutations - either static or using dynamic “knobs”. Once a story is pushed to master, the rest of the team can play with the feature on our static Storybook site, which is automatically generated by a post-deploy hook. After a component is built and styled, we integrate it with our GraphQL backend by writing a “connected” component using Apollo Client. This process is relatively quick because we know what all the components do what they should from using Storybook.

This approach allows our distributed team to work on the front end and back end independently for the most part. Near the end of development, we work together on integration and testing. We couldn’t be happier with this process and it’s really great for distributed and/or split teams. We’ll be writing more blog posts about our journey with GraphQL in the coming months.


A living style guide

We have gone all-in on using inline styles for our React components - not a single CSS file in sight! We landed on using Glamorous and may migrate to Emotion in the future. Some of the legacy code was using SASS and BEM which is a real pain when using React. We joke around that some of the components are a “wall of classnames”.

We haven’t come across a use case that couldn’t be solved with Glamorous. It lets us write dynamic, reusable, and composable styles. It works great with SSR and compiles down to optimized CSS. We have also configured it to write out the CSS in our Jest snapshots so we can detect style regressions.

Glamorous allows us to maintain a living style guide using exported JavaScript consts and some pre-styled components, which are available via Storybook. The master of record for all our colors, typography, breakpoints, etc. is in a folder of JavaScript code and visually available on our static Storybook site. It’s a wonderful thing.

We are also just starting to seriously adopt a standardized grid layout inspired by Material. We use this little util function to ensure our white space aligns to an 8px grid; which is easy to do when your styles are in JavaScript.

export const grid = unit => unit * 8;

Let’s say we need a panel that has 2 units of padding…

const Panel = glamorous.div({
  padding: grid(2)
});

We can easily search the codebase for usage of grid() and possibly even print warnings if the supplied unit param wasn’t a whole number.


Into the future…

We’ve made a lot of progress so far, but there is still plenty to do. It’s not possible to fix everything in one go, nor should you. We made some calculated decisions to leave certain parts of the site alone because there is a good possibility that they will be replaced at some point. There is no value in refactoring these parts of the site right now.

Modernizing StackShare’s front-end codebase has been an exciting journey so far and we have many more exciting steps ahead of us. We hope to share our ongoing journey with you through a series of featured posts.

Happy Stacking :)

- Rus