Technology

How the trust of a product owner let me rebuild the adidas product pages with 75% less code

August 12, 2020

Read time 7 min

Developers can be quite dogmatic when it comes to sticking to their principles. However, the workflow involved on complex projects often means that there is a lot of compromise required. In this blog, I explain how thanks to the trust of my product owner, I was able to rebuild the adidas product pages using only ¼ of the code in the original site, speeding up load time by 45% (and we all know how faster loads mean increased conversion and revenue). I was also able to stick to my belief that React is a view library and should be used as such.

I had already been working on adidas product pages for a couple of years when Reaktor proposed a complete redesign of this section. As part of this project, my product owner gave me a rare opportunity: to work alone on streamlining the tech side of these pages. This may not sound so special, but these kinds of development projects often have a bunch of stakeholders across both the agency and client: product managers, UX designers, branding people, etc. Because of this long-term working relationship, there was a high level of trust between the client and Reaktor, as well as between me and my product owner. This meant I had the freedom to devote pretty much 100% of my time to reworking the product pages. After starting last June, I showed the rest of my team a proof of concept a few weeks later in our Q3 planning session, and then spent the next 6 months working on it.

Naturally, as a codebase ages, it will be touched by dozens of people over the years, some of whom are long gone. It will go through many React versions and eventually ends up a bit of a mess. In this case, a lot of the issues with the old codebase were simply the result of data and display being tightly coupled. Much of the business logic resided within the React components, and a large portion was written using Redux. This meant that React acted more like a framework instead of a library. With the freedom to rewrite the entire page, I knew that I could clean this up according to a simple and well-established approach: using React only as a view library, separating the data and display.

Back to basics

One of the main principles behind the new architecture was to have the application fully “work” without any React code in place. In other words, you could delete all rendering code and still have the application make all the required API requests necessary to completely display the page (e.g. product data, inventory, etc.). 

Achieving this is simple: in the initialization sequence of the application – in our case fired by the router – all the React elements do is make requests and fire actions. That’s it! There are, of course, dependencies between the requests, but handling those is easy:


function init(dispatch, getState, productId) {
  const pProduct = dispatch(fetchProduct(productId))

  const pAvailability = pProduct
    .then(product => dispatch(fetchAvailability(product))

  const pCmsData = Promise.all([pProduct, pAvailability])
    .then(([product, availability]) => dispatch(fetchCmsData(product, availability)))
}

If that doesn’t seem hard… it’s not supposed to be.

Keeping the data pristine is a key priority of the application. What I mean by this is that the response coming from an API is essentially kept as-is when it’s stored. This, however, can easily lead into a jungle of isLoaded and hasError boolean values and additional error fields which further complicate any code and state. To combat this, I devised the Loadable<T> type:


import { Option } from 'fp-ts/lib/Option'
import { Either } from 'fp-ts/lib/Either'

export type Loadable<R, E = Error> = Option<Either<E, R>>

Effectively, this is just a tagged union with the states loading, error and ok. To make the usage of such values as simple and enjoyable as possible, I implemented a small function called match:


import { pipe } from 'fp-ts/lib/pipeable'
import { fold as oFold } from 'fp-ts/lib/Option'
import { fold as eFold } from 'fp-ts/lib/Either'

type Matcher<A, R, E> = {
  loading(): A
  error(err: E): A
  ok(result: R): A
}

function _match<A, R, E>(
  loadable: Loadable<R, E>,
  { loading, error, ok }: Matcher<A, R, E>
): A {
  return pipe(loadable, oFold(loading, eFold(error, ok)))
}

// Example usage in a component
type Props = {
  reviews: Loadable<Reviews>
  reloadReviews: () => void
}

function Reviews({ reviews, reloadReviews}: Props) {
  return match(ratings, {
    loading: () => <LoadingSpinner />,
    error: (err) => <ReloadButton onClick={reloadReviews} />,
    ok: (reviews) => <div>This product has {reviews.count} reviews.</div>
  })
}

I also wanted to use the match function in selectors, so I wrote an overload for it so that it can be curried as well:


type MatchArgs<A, R, E> =
  | [Loadable<R, E>, Matcher<A, R, E>]
  | [Matcher<A, R, E>]

export function match<A, R, E>(l: Loadable<R, E>, m: Matcher<A, R, E>): A
export function match<A, R, E>(m: Matcher<A, R, E>): (l: Loadable<R, E>) => A
export function match<A, R, E>(...args: MatchArgs<A, R, E>) {
  if (args.length === 1) {
    return (loadable: Loadable<R, E>) => _match(loadable, args[0])
  }

  return _match(args[0], args[1])
}

// Example usage in a selector, curried
export const getOverallRating = createSelector(
  [getRatings],
  match({
    ok: ratings => ratings.overallRating,
    error: () => 0,
    loading: () => 0
  })
)

In addition to these, some helper functions such as map and getOrElse were written to further ease the usage of such type. This abstraction has proven to be enormously useful, and is employed by many key parts of the application. A future thought for this would be to keep the previous value of the Loadable and pass that as an argument to the loading function as Option<T>, so that you still have access to the previous value while a new one is being loaded.

However, just having a bunch of data in your state doesn’t necessarily help – you must break it down into nutritious pieces of information. Doing this on React at the component level is sometimes okay, but what I tried to do is to minimize all logic on component level altogether. With the highly monadic shape of the state, composing selectors becomes highly efficient. So, we just put all the business logic there, aggregate the data, and the system puts out what we need.

An advantage arises from this: it allows the reuse of aggregated data across multiple components without having to pass the data from prop to prop. Each component receives its state straight from the source, further decreasing the amount of re-renders that would be caused by the passing of props. Another advantage here is that the logic for loading or aggregating anything can be changed without having to touch any components at all. As long as the type signature stays the same, you’re pretty much covered. For example, we recently changed the way our product data is loaded and stored within the whole single page application. All that was required to make this happen on the rewritten product pages was to change a couple of actions and some selectors. This ends up being pretty painless compared to the steps involved before.

One part that isn’t stored within the Redux store, however, is most of the visual state. From toggling a dropdown to keeping track of the current navigation item, this logic can and should be stored within the components. React is, after all, a view library. With the useState and useEffect hooks, this has become increasingly effective and fast to write.

So what is special or new in this implementation?

Nothing.

And that’s pretty much the point. The goal was to build the simplest version possible, with the least amount of code. By these standards, this project was a success. Just by using well established methods (effectively Flux, in this case) and not trying to over-abstract everything allowed us to achieve an efficient and fast product with about a quarter of the amount of code. Decoupling data and display also means that the designers and marketers can easily tweak visual aspects of the product pages without worrying about knock-on effects on the technical side. Over-engineering and building complex abstraction systems is also easy (and fun!), but for a system like this, why bother? Sometimes these things are supposed to be simple.

Never miss a post