Man searching on Zumper with laptop

At Zumper, we try to keep our technology stack up to date to provide a cutting edge experience to our users. The Zumper website has gone through many iterations over the years and, very recently, we completed our migration from Angular to React.

In general, the web changes completely every couple of years, with browsers continually improving. Any web developer with a few years’ experience has fond memories of rebuilding everything when the next big web framework comes out. The next big thing often means a better experience for the customer.

And so, at Zumper, we embarked on the long journey of upgrading our main web experience from Angular to React.

We’re excited to have the key aspects of our customer experience on a modern code base. Even more exciting, we were able to launch it without any client interruptions. It took 18 months and a monumental team effort before we fully rolled out React. While there are still some corners of our app that are managed by Angular, we will be migrating those portions to React in 2019. Going forward, all new features will be built using React.

Note: Throughout this article we refer to “server-side” versus “client-side” rendering. Server-side is what happens on Zumper’s servers before anything is sent to the browser, while client-side is what happens in the browser.

Server-side rendering

Out of the gate, we knew we needed to support server-side rendering. Server-side rendering can be a crucial performance enhancement, saving precious milliseconds during the initial page render. Server-side rendering can also improve the ability for search engines to crawl our content. Ultimately, it means we’re doing everything we can to help renters find the right apartment.

A typical React app is rendered client-side only. With client-side rendering, all of the content is loaded by JavaScript in the browser. That works for starter projects and simple applications, but it wasn’t a good fit for Zumper. A fair amount of our customers actually find us using a search engine. Search engines (and customers) favor websites that load the initial content quickly. We couldn’t just serve a blank page — we needed server-side rendering.

React has a good story for rendering components on the server and hydrating them in the browser. Very early in our journey we had an express server that could render our site on the server–and it worked well! The two sticking points we ran into were preloading the data and supporting code-splitting.

Preloading data into redux

We use redux as our client-side data store. In a client-side app, you might use react-router to decide which components to render, then dispatch an action to load the data for that route. Server-side you need to do the same thing, but it needs to be done before you render the component tree.

Initially, the confusing part about redux on the server was that we couldn’t dispatch anything to asynchronously fetch data. Server-side React rendering needs to be synchronous. We ended up following the data loading example in the react-router guide. However, the code provided in the docs is incomplete. We needed to mix the data loading example with the react-router-config example to come up with a complete end-to-end solution.

The trickiest part was ensuring that the data we pre-loaded on the server wasn’t immediately refetched on the client. We needed to support both server and client-side routing and keep track of when the app was hydrating versus when we were switching pages client-side (more on that below).

Our final code will eventually be open sourced. It allows us to do the following:

  • Determine which component tree matches our current route on the server
  • Preload data into redux (we allow for nested routes to independently manage their data-fetching needs)
  • Skip data loading on the initial client render
  • Trigger client-side data loading on route change

Supporting code-splitting

For code-splitting, we ended up going with react-loadable. This library allowed us to break the app code into smaller “bundles” using Webpack. On the client-side, this means that our customers only download the javascript code needed to render that page. On the server-side, this can cause problems.

Server-side, we didn’t want to lazy-load our components; React’s server-side rendering requires synchronous loading (see above). Additionally, on the client-side we needed to have all of the correct JavaScript code in place to avoid hydration issues.

The react-loadable docs have examples for configuring both Webpack and Babel to enable server-side and client-side code-splitting. The biggest hurdle for us was waiting for that project to mature. The first version we used didn’t fully support our use-case. Once we moved to version 5.x things were working much better!

High-level, it works like this:

  1. Preload all of the code-split bundles on the server
  2. Preload all of the data for the route into redux
  3. Render the components on the server; determine which bundles to include in the html; serve the html and preloaded data to the client
  4. Preload the bundles on the client
  5. Populate client-side redux with the preloaded data
  6. Hydrate the components

It seems complicated, but our data loading strategy and react-loadable made it super-easy.

It’s worth noting that React is coming out with a new React.lazy component that solves code-splitting on the client; but, of course, it doesn’t work for server-side rendering. They now recommend loadable-components for server-side rendering, so it’s very likely that Zumper will change the way we do code-splitting in 2019.

Client-side routing

Zumper on iPhone

Very early in our experimentation, we were able to get server-side rendering working for us. It took us much longer, however, to figure out how to enable client-side routing. React-router allows us to change the URL without actually refreshing the page (using the history package). This means that when customers interact with our page, we handle everything in the browser instead of making a round trip to the server. Client-side routing is a dramatically better customer experience, especially on mobile where page loading speed is very important.

Our first attempts weren’t fully utilizing react-router. Our code was a little complex and hard to manage, so we ended up getting back to basics. The key to our final client-side routing approach was considering the URL a first-class data source.

Initially, we were relying on redux as the one-source-of-truth. This made perfect sense for the initial page load because all of the data was preloaded by the server. However, that wasn’t necessarily the case once the client took over. We needed to pore over our app and ensure that, when the URL changed, we relied on that as the source of truth. Just like on the server-side, the client-side redux store needs to be derived fully from the URL.

The first step was reworking our data loading strategy to cover the use cases for both server-side and client-side (see above). As a refresher, that meant that we would use the preloaded data for the initial page load and then fetch data on the client after any URL changes. We ended up dispatching an action when the app component first mounts and using that to know if we were hydrating or not.

The second step was reworking our code to respect the URL above all else. This ended up being a huge undertaking because we initially went down a different path. We had some high-level components doing some extraordinary work to reconcile the URL and the redux state (which involved heavy abuse of componentWillReceiveProps). By switching to a URL-first strategy, we were able to strip hundreds of lines of code out of those components and stop relying on deprecated functionality. It took some significant testing to ensure we didn’t break any existing functionality, but ultimately this resulted in simpler, more maintainable code.

Customized react-router

We needed to support some advanced routing scenarios which relied on regular expressions that were more involved than what react-router supported out of the box. We ended up publishing our own @zumper/react-router package on NPM to enable this. Under the hood, this exposes a method to compile custom regular expressions for a route. The underlying packages that react-router uses for route matching already supported this functionality; we just exposed it for usage in a normal route.

We’re not done yet

We’re not stopping here. We continue to pursue easy performance wins and we’ll be rolling out some of those (hopefully) before the end of the year. 2019 will be an exciting time at Zumper, as we migrate the remainder of the Angular experience to our React codebase. Even more exciting, we will be expanding what Zumper offers customers as we continue to revolutionize the experience of finding and renting an apartment. And now, we can do that on a solid, state-of-the-art foundation.

Interested in joining the Zumper team? We’re always searching for bright, passionate, hard-working engineers, designers, and business leaders to help us build extraordinary products and revolutionize this industry. Check out our current career opportunities or email us at jobs@zumper.com.

Find your next place