Instacart, React, Dynamic Themes, and Inline Styles

Written by Will Faurot and edited by Lovisa Svallington

Oh my.

Powered By Instacart themes

2017 brought lots of interesting technical challenges at Instacart, but one in particular was fun and unique: handling retailer-defined themes for our Powered By Instacart product.

instacart.com and the Android/iOS apps are primarily a "marketplace", meaning users can shop and order from multiple retailers.

We also offer a product for retailers known as "Powered by Instacart" (PBI for short). For a Powered By Instacart site the experience is retailer specific - including the site's color theme.

publix PBI

Note: we also have PBI native apps! But here we'll focus on the web side.

In 2017 we improved the experience for retailers by allowing them to update their theme via a dashboard. Any changes made should then be reflected immediately after a site refresh, there should be no need for a new build/deploy.

We also wanted to make things as easy as possible for engineers doing day-to-day UI work. Having to remember theme-specific implementation details is a burden. Devs shouldn't have to worry about accessing theme data or knowing which properties map to which elements. "Themeable" elements are something we can and should abstract into a set of React components. A perfect scenario for us was a component for each themable element -- importing a Button or a Link should be all it takes to do theme-friendly work.

Here's how we did it.

CSS vs. inline styles with JavaScript

This project coincided with us starting to rethink how we wrote styles altogether. Most of our styles at this point were written in SCSS and compiled by the asset pipeline, but we were excited by the trend towards componentization and collocating HTML/CSS/JavaScript, driven by React and tools like css modules, styled-components, and Radium.

The idea of "separation of technologies is not separation of concerns" (loosely quoted from Pete Hunt) made sense to us. Our logical unit should be a component, not one individual aspect of a component. When we collocate markup, styles, and behavior they can be packaged and reused more easily, it's easier to find and eliminate dead styles which helps keep assets lean, and any scoping/name collision concerns with CSS are a non-issue (something we originally solved with naming schemes like BEM).

At Instacart we love React, so naturally we'd seen Christopher Chedeau's talk about "CSS in JS" and decided to look into styling React components with JavaScript.

As we were brainstorming solutions for themes an idea was thrown around. Arguably the hardest part of this problem is handling programmatic theme updates via a retailer dashboard. We knew theme data could be stored as a JSON object in our Postgres database:

{
  action: "#ab4543",
  actionHover: "#b45455",
  primaryBackground: "#c76787",
  primaryForeground: "#d34332",
  // Other themeable stuff...
}

But where do we go from there? Incorporating this into a proper stylesheet we can fetch with a link tag means we'll need an extra step that defines Sass variables and generates new minified assets on the fly. We'll also need to push them to our cloud storage/CDN.

What if we tried inline styles with JavaScript? We soon realized this hit most of our marks:

  • All styles exist in the same universe. We can define base styles for themeable components, and retrieve a retailer's theme style object however we wanted (including asynchronously).
  • Handling theme updates by retailers is easy - just update the JSON object whenever a change is made. Because we'll be sending the JSON directly to the client there are no extra build steps!
  • Theme styles can be fetched asynchronously.
  • By creating the right abstractions, we can make sure developers don't have to sweat implementation details around themes.

What we didn't want was many different ways to style content. If we went down this path, we really had to go for it. The good news is that we found plenty of other reasons to like inline styles. This is worth a post all its own, but here's a tl;dr for now:

  • Coupling of styles to components means fewer excess styles and competing styles.
  • Views are encapsulated in one location with one technology (JavaScript) as opposed to HTML/JS/CSS in three different locations. Less context switching is great.
  • Webpack's tree shaking makes it easy to removing unused styles, ensuring we don't serve any unecessarily bloated assets.

An introduction to the software that makes it all possible

For some context before we get too deep into the details, here are the three major code additions that made all of this possible:

  • Themer - a class for registering and validating themes. It has a simple event system so React components (or any component in a JavaScript environment) can subscribe to theme updates.
  • withTheme - a higher-order component which uses a Themer instance to inject theme props into React components.
  • A set of theme-aware components like Button and Link as part of our internal component library, Snacks.

Snacks components define a base set of styles, and receive any theme-related styles from the server. Merging JavaScript style objects is all it takes to apply a theme. Pretty cool.

How it works at a high level

I like to think about this in terms of the lifecyle of a single request, so let's go through one step-by-step.

  • When a user visits a Powered By Instacart site (e.g. delivery.publix.com), fetch the retailer's theme styles out of the database.
  • Fetch the theme from the API when the client is initializing .
  • When the above request completes, load the theme styles into a themer instance.
  • At this point there are two options:
    • Use one of the provided Snacks components which will take care of applying the correct theme (e.g. Button).
    • Use the withTheme higher-order component if you need theme information and no existing components are available.

Let's look at each of the client-side components that make all this possible in more detail.

Themer

Themer is a small utility class that stores an active theme and allows users to register callbacks that a Themer instance will fire whenever the active theme changes (which enables dynamic theme swapping, i.e. without a page refresh). It also has a few validations which check that a given style object has the right fields, and that those fields have the right values. Snacks, our component library, also exports an instance of Themer (a singleton). When a user visits instacart.com or a PBI site like delivery.publix.com, the Instacart app imports the singleton and sets the current theme (which was provided to us by the server).

// ...This is somewhere in our initialization code when the app
// boots for the first time.

// Extract the style object from the retailer configuration
const { colors } = response.currentStoreConfiguration.styles

// Register the theme with Themer. This will update themer's
// internal state and pass new theme data to any listeners
// who are subscribed via the `withTheme` HoC, more on
// that in a moment :D.
themer.themeConfig = {
  colors: {
    action: colors.action,
    actionHover: colors.action_hover,
    primaryBackground: colors.primary_background,
    primaryForeground: colors.primary_foreground,
    ...
  }
}

Themer felt like more of an implementation detail and not something developers should be directly interacting with too often. We ended up with a new withTheme higher-order component to provide a better API for developers who wanted to access theme data in custom React components.

withTheme

withTheme encapsulates themer related logic, and injects a snacksTheme prop into any component it wraps. snacksTheme is an object which maps directly to the retailer theme object from the database that we fetched earlier in the cycle.

withTheme is heavily inspired by withStyles, a higher order component that's part of Airbnb's react-with-styles.

To use it, import withTheme, wrap your component, and apply any theme styles as necessary. Here's a simple example:

import React from 'react'
import { withTheme } from 'ic-snacks'

function MyCustomComponent(props) {
  return (
    <div style={{ color: props.snacksTheme.colors.priamryBackground }}>
      {props.children}
    </div>
  )
}

export default withTheme(MyCustomComponent)

Here's how withTheme works:

import React, { Component } from 'react'
import themer from './index'
import { themePropTypes } from './utils'

function withTheme(InnerComponent) {
  class Wrapped extends Component {
    static displayName = `withTheme(${InnerComponent.displayName})`

    static propTypes = {
      snacksTheme: themePropTypes
    }

    // When the component mounts, attach a callback to the `themer`
    // instance. The `onThemeChange` method will be called when the
    // current theme changes. themer.subscribe also returns an
    // unsubscribe function so we can clean up listeners for any
    // component that unmounts.
    componentDidMount() {
      this.unsubscribe = themer.subscribe(this.onThemeChange)
    }

    componentWillUnmount() {
      this.unsubscribe()
    }

    // When the theme changes, manually re-render this
    // component (and by extension the wrapped component).
    // Very similar to how `connect` from react-redux works.
    onThemeChange = () => {
      this.forceUpdate()
    }

    // Render the wrapped component and pass it the snacks theme prop!
    render() {
      return (
        <InnerComponent
          {...this.props}
          snacksTheme={themer.themeConfig}
        />
      )
    }
  }

  return Wrapped
}

export default withTheme

Theme changes are dynamic without too much added effort. Under the hood withTheme will call forceUpdate to manually render itself and by extension any wrapped components when the current theme changes. This is great for additional theme dimensions like high-contrast colors, which are configurable via a user's account page.

withTheme is great for applying theme styles to custom components, but the best developer experience meant adding one more set of wrappers by creating a suite of common components as part of our internal component library.

Just importing a Button or a Link component, without needing to worry about themes, is the dream.

Snacks

Snacks is Instacart's React component library. Even without the added benefit of handling themes, maintaining a component library means visual consistency and lets developers move faster by extracting work common to all UI development.

We handle as many theme-related concerns as we can at this layer because it's the easiest API for developers to learn and use. If I need a button for my new feature, I can import a Button from Snacks and not even have to think about themes.

<Button snacksStyle="primary" size="large">
  I'm a button
</Button>

I don't even need to know if we apply themes via classes, or using JavaScript and inline styles. It just works.

This Button will work out of the box in any configuration, whether it's the Instacart marketplace, or any of the various Powered By Instacart sites.

How have inline styles been working out?

Would recommend. This system as a whole is great both for retailers and for Instacart. Powered By Instacart retailers have a high level of control, meaning any configuration management can be handled on-demand and by anyone with access to the dashboard. It also helped us launch new Powered By Instacart sites extremely quickly by reducing the amount of manual engineering effort.

Inline styles certainly have their flaws, which we'll leave for another day, but we're incredibly happy with our choice so far. Can't complain about that.

You can find me on twitter. And if you like deep learning and/or emojis, be sure to check out the Instacart engineering blog.

The contributors to JavaScript January are passionate engineers, designers and teachers. Emily Freeman is a developer advocate at Kickbox and curates the articles for JavaScript January.