Skip to content

Blog


Building the Caviar Web Experience Using Reusable React Components on the DoorDash Platform

September 29, 2020

|
Hana Um

Hana Um

DoorDash’s acquisition of Caviar in 2019 afforded us the opportunity to increase efficiency by running Caviar on our existing platform. However, as we also wanted to preserve the Caviar ordering experience for its loyal customers, we rebuilt the Caviar web experience using React components on top of the DoorDash platform.

While DoorDash offers a broad array of restaurant and merchant options, Caviar focuses more on the local premium and food enthusiasts. Before the acquisition, it developed a customer base with expectations about the service, and we needed to maintain the features they had become used to. 

One challenge with this project involved serving the correct web experience to site visitors, making sure that DoorDash customers could order from DoorDash-supported restaurants and Caviar customers could order from Caviar-supported restaurants. As we planned on serving both experiences from the same platform, with a shared data infrastructure, this hurdle was not trivial. Likewise, we needed to engineer our React components to preserve the Caviar look and feel.

Despite the challenges around merging the two companies, navigating new codebases, and working remotely, our team was able to come up with scalable technical solutions to support the successful launch of a fully realized Caviar consumer web experience.

Two web experiences, one backend

The Caviar acquisition required that Caviar and DoorDash engineers work together to create a seamless, coherent experience for our customers by migrating Caviar onto the existing DoorDash systems. While Caviar and DoorDash would share a common backend infrastructure, the shared frontend client would support two different experiences.

A major goal of this project was to preserve the Caviar look and feel. In order to achieve this, we had to figure out a way to build two different experiences using the React-based frontend technology on which we had built the DoorDash web experience.

Figure 1: DoorDash established its web experience to help customers easily find and order the food they desired. The DoorDash frontend relies on React components to serve a dynamic, personalized experience.

Figure 2: When re-building our Caviar web experience, we wanted to maintain the look and feel that its customers were used to. However, we also wanted to gain engineering efficiency by building it on the same architecture as the DoorDash web experience. 

Scoping the project

There were three main challenges in determining how to incorporate a new Caviar experience:

  1. Identify the experience: We needed a simple way to determine which experience the user would receive on page load, Caviar or DoorDash. 
  2. Build reusable frontend components: Once we found a way to determine the experience, we needed to build the necessary components within the existing React web codebase to be utilized any time a new experience filter was needed. 
  3. Minimize disruption: Last but not least, we needed to avoid changing any major flows or causing dependency issues for ongoing DoorDash projects. This included keeping existing APIs and data structures that were queried from our Apollo GraphQL-based backend for frontend (BFF).  

The current frontend system at DoorDash

Figure 3: Two main repositories make up our DoorDash frontend. One consists of an Apollo GraphQL server running data queries to the backend, while the other is the web-based user interface.

Our current DoorDash consumer frontend system consists of two main repositories. The first is our Apollo Graphql BFF server that acts as a middle layer between the client code and the backend code. This middle layer is where we establish all the queries and mutations required to fetch data from the backend through a combination of REST API and gRPC calls. 

The second piece is the client application code itself which lives in our web monorepo. This is the main frontend user interface (UI) layer that displays the frontend data fetched from the BFF. It is within this UI layer where we would add our new components to determine which experience to provide our users.  

Building the experience

Once we scoped and designed the project, we could begin building. Rather than working from a blank slate, we had the DoorDash web experience available as a model. To integrate with the existing React architecture, we had to think about creating reusable components that could be easily utilized anywhere in the codebase that required a unique Caviar experience. 

Determining which factor chooses the experience

The new Caviar web experience would consist of a whole new look and feel that is separate from the DoorDash experience. Caviar would use a unique design, color themes, content, and text. We needed a way to showcase these new elements to our Caviar users without disrupting the existing DoorDash components.  

After multiple discussions, both frontend and backend engineers decided to build this new Caviar experience around the availability of a header response being set to either “caviar” or “doordash”. When a user visits the Caviar web site, trycaviar.com, Cloudflare, our web services vendor, catches the request and sets the header to “caviar” so our app-consumer codebase can determine the experience in the UI. Our page rendering service (page-service) grabs that information and initializes the client app with the new header value. We then configure all client calls with a header similar to x-brand=caviar that is passed to the APIs.  

Having come up with a mechanism to set the experience, our main goal became creating the necessary React components by utilizing modern React patterns to let developers showcase Caviar or DoorDash specific features. 

Implementing a React Context

To be able to capture the correct web experience and have it accessible by our React components, we created a React Context that would serve as the source of truth and hold the experience state. 

export enum Experience {
DoorDash = 'dd',
Caviar = 'cav',
}

export interface IExperiences {
experience?: Experience
}

const createNamedContext = (name: string) => {
const context = createContext({} as IExperiences)
return context
}

const ExperienceRouterContext = createNamedContext('ExperienceRouter')

The current web monorepo had a mixture of newer functional React components and legacy class-based React components. In order to support both of these interfaces, we had to create two ways of providing the experience.

To support the functional React components, we created a hook that would grab the experience directly from our context in order to provide the experience to a component.

const useExperience = () => {
const { experience = Experience.DoorDash } = React.useContext(
  ExperienceRouterContext
)
return {
  experience,
  isDoorDash: experience === Experience.DoorDash,
  isCaviar: experience === Experience.Caviar,
}
}

We can now call this hook anywhere in a functional component to access the experience. Here is a snippet of a component that calls the hook to determine if we’re in the Caviar experience:

const Header: React.FunctionComponent<Props> = props => {
const { isCaviar } = useExperience()
return (
  <Root>
    <FixedPanel>
      <HeaderSection>
        {!props.isEditOrderCart && (
          <HamburgerButton
            data-anchor-id="HamburgerMenuButton"
            onClick={props.onClickHamburger}
          >...

And to support the legacy class components, we created a provider that wraps around the useExperience hook since legacy class components cannot use a hook directly. The provider passes the experience as a prop to the children components. 

interface IProps {
children: (experienceShorthand: {
  isDoorDash: boolean
  isCaviar: boolean
  experience: Experience
}) => JSX.Element
}

const ExperienceProvider: React.FunctionComponent<IProps> = ({ children }) => {
const value = useExperience()
return children(value)
}

export default ExperienceProvider

We can now use the provider to wrap any component that requires access to a specific experience. Here is a snippet of how we use the wrapper in a class component:

return (
    <ExperienceProvider>
      {({ isCaviar }) => (
        <MediaQueryProvider>
          {mediaQuery => (
            <Root mediaQuery={mediaQuery}>
              {this.props.storeFilters.map((filter, index) => {
                return (
                  <div>
                    {this.displayFilter(filter, index, isCaviar)}
                  </div>
                )
              })}
            </Root>
          )}
        </MediaQueryProvider>
      )}
    </ExperienceProvider>
  )

Throughout the Caviar integration process, we wanted to provide clear, self-documenting interfaces that could be used throughout the codebase. One such example is the ShowOnlyOnDoordash component. This component provides a simple reusable interface for DoorDash engineers to use while leaving a small footprint in the codebase. 

class ShowOnlyOnDoorDash extends React.Component {
public render() {
  const { children } = this.props
  return (
    <ExperienceProvider>
      {({ isDoorDash }) => (
        <React.Fragment>
          {React.Children.map(children, child => {
            return isDoorDash ? child : null
          })}
        </React.Fragment>
      )}
    </ExperienceProvider>
  )
}
}

Another challenge we had was implementing changes and refactoring already existing DoorDash components. The strategy we used was to make changes to the internal code but maintain the same interface. 

A good example of this was our shared NoMatch component. The Caviar team needed a separate NoMatch experience but we didn’t want to make a large refactor to a crucial component. Instead, we split the NoMatch component into NoMatchDoordash and NoMatchCaviar internally. This way, the NoMatch interface can still be used anywhere with no disruption, but the internal workings would display the Caviar or DoorDash NoMatch component based on the correct experience.

const NoMatch: React.FunctionComponent<Props> = props => {
const { code, error } = props
const { experience } = useExperience()
const correlationId = getRequestId(error)

switch (experience) {
  case Experience.Caviar:
    return <NoMatchCaviar />
  default:
    return (
      <NoMatchDoorDash
        code={code}
        correlationId={correlationId}
        showFullErrorDetails={isOnCanary()}
      />
    )
}
}

Conclusion

By keeping DoorDash’s current architecture top-of-mind during this project, the Caviar web team was able to create multiple reusable components to provide our engineers with the proper interfaces to access the experience state. By providing this simple pattern, we were able to develop freely on Caviar-specific features without disrupting existing DoorDash features. This way, we preserve separate DoorDash and Caviar experiences for our users. 

Our current pattern can also serve as a foundation for other experiences that may be incorporated into DoorDash in the future. We ultimately wanted a common design pattern that encapsulates logic and state in order to provide a clean interface for the developer to implement different experiences. Utilizing reusable React components and React Context allowed for us to easily access shared state across different parts of the application.

Acknowledgements

Shoutout to Ting Chen, Keith Chu, and Monica Blaylock for being amazing teammates and for all their hard work on this project!

Related Jobs

Location
San Francisco, CA; Seattle, WA; Sunnyvale, CA
Department
Engineering
Location
San Francisco, CA; Seattle, WA
Department
Engineering
Location
San Francisco, CA; Sunnyvale, CA; Los Angeles, CA; Seattle, WA; New York, NY
Department
Engineering
Location
San Francisco, CA; Sunnyvale, CA; Seattle, WA
Department
Engineering
Location
San Francisco, CA; Sunnyvale, CA
Department
Engineering