Building a Scalable SPA with React

Nathan Zhang
carsales-dev
Published in
8 min readOct 8, 2019

Instant Offer has been a successful carsales product for years and it plays a significant role in terms of company revenue as well as branding. Last year our team received a new business flow and we started to rebuild the app from scratch. I would like to shout out to our backend and QA engineers who responded quickly with robust deployment processes, notification flows, detailed logging, error handling as well as reliable API endpoints. This strong support allowed me to explore confidently during the rebuild and now I want to share my experience and lessons learned in this article. You might not find any new theories here, but what was concluded could possibly help a front-end developer be better prepared for potential technical challenges and non-technical uncertainties.

Introduction to Instant Offer

Instant Offer is an online vehicle trade-in application that guides users to find their vehicle and submit vehicle and contact details before they are presented with a quoted price or a price range. The user’s journey continues after they accept an offer! They can book car inspections to get a better feel for the car, and they can also opt in to get helpful updates on their favorite cars until they’re handed over and sold.

To allow the user to track the offer status, we’ve provided comeback flows so that users can revisit the Instant Offer website from notifications or from the membership dashboard.

The Instant Offer website is also connected to different internal products like:

and accepts the user from them.

Decision making

So when I received this task, I saw three major requirements:

IO Frontend Flows
  1. Instant Offer needs to allow users to move forward or backward in the process, and the app needs to save users’ status so that they won’t lose what they have done if the page is refreshed.
  2. Our UI components need to meet the carsales design guide, and they need to be ready or close to being ready to be published as UI libraries.
  3. For each step or a UI action, we need to send corresponding GA events, e.g. page view events or click events, to our analytics team.

Also, a foreseeable challenge I found is those steps might be swapped or removed or added with new steps depending on business instructions.

With the above mentioned in mind, I started a boilerplate with React and .NET Core.

Keeping your state

To address the first consideration, I chose to save step data in session storage so when the browser page is refreshed, the app still knows where the user was last time. For those who would like to send browser cached data to the backend, or even save them in a database for further analysis, using cookies is the best option.

Regarding global state management within the React app, I chose React Context (which brought us more effort in mocking them when it came to unit testing) rather than Redux, and I defined the app level state in Provider.js.

Provider.js

The state object is passed into the child components as propswith which we can call back to update the global state as unidirectional Redux does:

this.props.context.dispatch(ActionTypes.NEXT_STEP)

or

this.props.context.updateUI(ActionTypes.PAGE_LOADED)

or bypassing some payload

this.props.context.dispatch(ActionTypes.GO_TO_STEP,StepNames.Final)

But how do we fetch global state from components? That is the Context’s job :

With the above functionalities implemented, I’ve made a step page with the following fundamental behaviors:

General behaviors of a step page

The Page component is not concerned with what is the next page, what is the previous page or what does the program do when PAGE_LOADED is dispatched. Instead, the Reducer function will handle these requirements.

Reducer.js

Up until this point we’ve built the road, but we don’t have any traffic control. To centralize step flow management in one place, we put all the logic in a function and you may interpret it as a hub, which decides where the traffic goes and what pages need to be loaded.

StepConfig.js

Single Response and Open-close principles apply in IO frontend architecture. With page independence and streamlined step management, Instant Offer can dynamically adapt to comprehensive business flows. Which proved to be the fundamentally most correct decision I have made. As time goes by, we saw different sets of ideas and multiple versions of designs added to or modified the original UIs, but IO was flexible enough to stay robust and well sorted.

As a software engineer who translates business requirements to code, we should own the responsibility of code reusability and readability. We need to address concerns or even push back inconsistent or none streamlined ideas sometimes. Those random instructions could hurt code simplicity, increase unexplained context to your code and make it harder to test and handover to another developer. Those none-tech factors can not be ignored and it needs us to positively engage in conversation with designers, product managers and stakeholders when you receive a new requirement. Raise your points about the programing restrictions, performance concerns or maintenance cost or other considerations to in the team discussion. Clarify as many details as you can, which can help better planing your tasks and improve efficiency.

Come back to our app, now we can easily step over or back or insert new steps. As a result, our app can quickly adjust to any new requirement, eg: integrating users from carsales TradeIn or swap personal details and vehicle details step.

Make your SPA a transformer

The building blocks

The second consideration is about UI components. To describe what I mean, here is an analogy: imagine you are building a car from scratch, what do you need? Firstly, you will need a body and a chassis, which is what we’ve done in the above section; Also you will need all the parts to get the vehicle running, for example, wheels, engine, gearbox, battery, etc.

The body and chassis can be unique per car model because cars are normally designed with characters and individualities. But, the parts should be universal if production cost is considered. The same idea applies to UI components design, and UI components are the “universal parts” I mentioned. We love to see one dropdown following one style guide shared by multiple projects. But we hate to see each project hosts its own dropdown with the same but duplicated style.

A popular way of designing reusable components is by following Atomic design patterns which breakdown your components in multiple groups. For example, a Button is an Atom, a validation filed is a Molecule, a select car component is an Organism, etc. Structure your code folders like this and bring the whole team on the same page, which will be a huge benefit for program scalability and flexibility in the long run.

Also, it is recommended to build your UI components with an assumption that anyone can move the whole folder of a component to another project and without particular wiring or setups. To allow that to happen you will need to avoid some bad practices, for example :

  • Having webpack defined global constants in components, as those variables only live in transpiling time for a certain build and can’t be shared across.
  • Or have any browser objects in your components for example, window or document or setTimeout(), which will break in server-side rendering.
  • Or put Context.consumer in your component rather than passing global state via props, or making your component too smart.

Those valuable preparation works will help your team build a React component library in the future, which offers your project an opportunity to scale up. It might cost some time in the beginning, but as time goes on, you will soon find out all the effort is rewarded with less unknown context, better scalability possibilities, little maintenance cost and greater confidence in your functionalities which is definitely worthy.

Low maintenance scenario

Making your SPA ready for trackings

The best product is never born as the best. After we released the new Instant Offer at the start of 2019, our revenue hit the lowest point in history. But based on Google Analytics, we kept AB testing and improved our app bit by bit which gradually turned the product around. By this July, we tripled our revenue and the continuous user analytics has given us great confidence in many business decisions.

To have a tracking friendly React component, you can create a parent component with basic functions, for example, “sendPageView” or “sendClickEvent” and extend it from other pages.

class YourOfferPage extends GATrackingWrapper

It is very litter effort but saves a lot of duplication, which also answered the third consideration.

Also, to improve the core vitals, we could optimize the loading by adopting three approaches:

  1. From webpack.config, we define code split and break the bundle into multiple chuncks : eg, vendor, react and home (just the first step).
  2. Also, dynamically import later steps which will further split the bundle.
  3. Use the latest image format and optimize to improve the largest contentful paint.
  4. Smartly using memoization to avoid unnecessary renderings.

Tech SEO optimization is a continuous effort and we will not cover all the approaches here.

You might not know what you exactly want

In reality, we don’t always know what is best for our product and some times the whole team is learning while doing it. So adapting to market and user feedbacks quickly is a necessity. To make that happen we need to think of software engineering as similar to building a house by putting brick by brick together. But that is just a close analogy and not exactly right.

Building a house is an irreversible process. For example, once you have your foundation established for a four-level building, you can’t put on seven levels; or if you had a blueprint of a residential apartment, you can’t improvise on the fly and turn it into a shopping center.

But software engineering practices composition, reuse and flexibility. Think of Lego, where you can tear down a whole building and reassemble it in a better way with tolerable effort. Which requires quite a lot of patience and intelligence.

Using Instant Offer home page as an example, each section of it was given a dynamic order so we could have multiple Optimizely driven versions, the analytics team observed the comparison and finally called out a winner.

Single response components and flexible css layout allow such kind of product tests to happen so putting the effort into code quality, which seems not present as immediately relevant to the customer experience, is very important and ultimately can help us provide more customer value faster as we make changes.

--

--