Here at Handshake, we’re always working to improve our end-user experience, and a key part of that is making it easier for developers by having a great toolset and stack. Among other things, that means that we’ve been busy converting significant parts of our product to React, which involves a complete rewrite to enable better performance, interactivity, and stability.
In our last blog post about React, we had just finished our first major React project, and we were looking forward to improvements we could make in the next one. Six months and several projects later, we’re excited to share our progress, especially around how we use Redux.
We’re in React: Now What?
As we stepped back and evaluated our first completed React project, we knew that the library had proven its value, with a host of major features that dramatically upgraded our client-side experience. However, it was also clear that there were additional tools and libraries which we could layer on top of React to provide a better experience for our developers.
While many areas were highlighted for future improvement, special needs were seen around shared component state, routing, testing, and applying CSS styles. We have made significant progress in all areas, for this blog post we’ll focus on the improvements we made to the backing state that dramatically improved attitudes toward React among developers, and accelerated adoption on all parts of our application.
Early on in our React development, we realized that we needed a solution for shared state management. React does not require any specific implementation for a state shared across components, or even to have that type of state at all, but most React apps of even basic complexity can benefit from it. We knew that the only way to scale our React usage to encompass the entire site would be to implement a shared state.
We felt it was important to evaluate a few different options for the state. Since Redux was the clear front-runner from the beginning, we evaluated the other options against its strengths and weaknesses.
- Knockout. Since we were migrating from an existing Knockout codebase, we wanted to see if it was feasible for Knockout to continue to own the viewmodel, and have React just implement the user interface. Unfortunately, this did not solve many of the issues we encountered using Knockout, and since the two libraries aren’t commonly used together, we would have to solve any problems we encountered ourselves. We felt this path was untenable and moved on.
- Flux pattern. Since we knew that the overly-coupled actions and stores were more complicated and difficult to use than the equivalent concepts in Redux, we didn’t feel that this was preferable, and ruled it out.
- MobX. A lesser-known choice in the React community, MobX uses many of the same concepts as Knockout, such as observables, and is designed to be integrated into React. We briefly evaluated this library, but were hoping to move away from the observables pattern and did not take a closer look.
- Relay. This was an attractive option for us, and GraphQL was very appealing as well. However, we didn’t want to spend the time to rebuild all of the necessary endpoints to use GraphQL, and that took Relay out of scope for this project. However, we are open to using it in the future if we get an opportunity.
We ended up deciding that Redux was the best choice. Between the incredible tooling available and the simple yet powerful architecture, we felt that there were no alternatives that even came close to the benefits Redux provided. We’ve continued to build upon the functionality that Redux provides, implementing support for hot reloading and time travel debugging in our codebase. The strong ecosystem and momentum around the library were also key for us – we’ve found many supplementary libraries and tools that would not have been available to us if we had made a different choice.
Redux In Practice
Our first project with Redux was a redesign of our student profiles. Using Redux allowed us to quickly build the page from scratch – we loaded data via a newly-built set of endpoints and plugged it into the store on load, with each endpoint corresponding to a component on the page, and getting its own key in the Redux store. Within those keys, each component could organize the data however it wanted.
We evaluated a few additional libraries to help with the student profile, and decided to try out Redux-Form. This library provided a standard way of handling validations, hints, and styling across all of our forms, while tying into Redux for state management. With the help of Redux-Form, the student profile launched on an aggressive schedule and was considered a big success.
As we moved on to our next React project, we decided to tackle our document management process, which actually contains two separate page designs – the documents list, and the pages for individual documents. We explored using React Router for the first time on this project, but having multiple pages in the same React root added complexity to our store structure. At this point, we began to feel that our laissez-faire approach to the store was falling short.
Since we were developing the documents project as a single-page app, using React Router, the document list and individual document pages shared the same store. As it was built, we needed to refactor the state’s structure multiple times to support all of the functionality and to ensure there was a single source of truth for the data. This ended up being a significant time sink, and we decided we needed more up-front planning to alleviate these issues going forward.
More Pages, More Problems
Around this time, several other React projects were starting and encountering the same issues with the store. Each project was building their store in a different way, and unique and unintuitive store structures started to appear across the codebase. This was worrying, because being able to intuitively understand the store structure when working on React projects across the app was crucial to our continued expansion of React.
It was time to standardize our Redux store for all new projects. After some research into Redux patterns for multi-page apps, we were unable to find existing solutions – Redux is mostly used for individual pages, or in single-page apps. We had to find the appropriate store structure to use across the site, and we were going to have to invent it ourselves.
A Global Solution
The store structure that we came up with is meant to be flexible enough to be shared by every page and project root that uses Redux. It looks for shared features and functionality from each project we’ve already built, and consolidates it into a single universal tree structure. The pattern should allow for interaction on a page, and should continue to be a good fit as we transition to a single-page app.
As a complement to that, we introduced a basic http library that allowed us to standardize transferring data between the server and the shared store with minimal headaches, taking care of common tasks such as camel-casing and error handling. This allowed us to use our existing JSON-based endpoints to get data for our pages into the global store without additional work.
Let’s take a quick tour of each top-level key in our store structure:
This portion of the store is master storage for all data that we download from the server. When data is pulled from the server, it is automatically fully normalized and put under the appropriate sub-key, allowing us to quickly look up any data needed. We use Normalizr to do some of the heavy lifting for us. Data changed in this key can then be automatically updated to the server.
We built standardized actions and reducers to work with this key and eliminate all of our boilerplate code around creating, updating, and deleting database rows – one of the biggest wins from the entire project for developer productivity.
Redux-Form version 6 requires a top-level key to put its form data under, and by default it is
form – which fits exactly with our notion of the global store. Each individual form’s data is saved under a sub-key named after the form in question and managed by the form.
This key is used for any page-specific data, or data specific to a certain component on the page. One example of this is for storing a set of IDs for the elements visible on the current state of the page – you may have downloaded twenty different jobs, but only five are currently visible in the “related jobs” section of the page. We’re still evaluating how to most effectively use this key.
Canonical permissions data for the current user is stored under this key.
This is a specialized version of the
page key that is used only on search-related pages. We have many search pages, and we wanted all search pages to share the same data structure, so we felt it made sense to give them their own key. This includes search filter data, which search results are displayed, and pagination information.
This is where we store the current user’s ID and other details that are relevant across all pages.
This is an object containing the feature toggles that have been enabled for the current user that are relevant to the client-side experience.
For pages using React Router, we include this key to include the values it needs, such as the path and model.
We’re just beginning to settle into the global store, and we expect that it will continue to evolve over the next few months as we move more of the Handshake app to React and see where it needs improvement. Already we have eliminated and added keys to the global store, but the difference is that we now do it across all Redux projects on the site, so that they continue to be consistent.
As we continue to explore and build on the immense ecosystem of React and Redux, we’re always finding better libraries and tools to use. A future blog post will explore our use of Redux-Form, which we’ve found to be a valuable ally in our quest for standard form code, both in design and functionality. We’re also in the early stages of planning a robust routing system across different pages in the app, and are exploring more ways to eliminate boilerplate and simplify our actions and reducers. Maintaining a great React and Redux experience for developers and end users is a never-ending job!
If you’re interested in building a great user and developer experience on top of a first-class React and Redux stack, we encourage you to apply for our Full-Stack Software Engineer position. We are especially interested in candidates who are excited about changing the job finding experience for college students across the country!