A/B Tested, Feature Flagged, White-Label Apps with Webpack

Andy Weiss
RBI Tech
Published in
10 min readJun 17, 2020
open box

For most of us, the thought of tweaking our app’s Webpack configuration inspires about the same level of excitement as a bi-annual trip to the dentist. We know it’s important, necessary even — certainly better than the alternative — and yet, the goal is mostly to get in and out without anything going wrong. The popularity of Create React App and other tools is largely built upon their ability to abstract away these configuration details and allow developers to focus on shipping features quickly.

But the better we know our tools, the more likely we are to recognize when they fit a particular problem we face in our work. For a certain class of project, it’s worth considering Create React App’s defaults as a starting point, rather than the end of the conversation. Thoughtfully applied, some small modifications can unlock a world of possibility.

For example, we often know at the outset of a project that we will need to build and deploy different versions of our application. Whether it’s for A/B testing, feature flagging, targeting specific platforms, or applying a custom theme, we want the ability to maintain multiple parallel universes around a single codebase without unnecessary duplication or complexity. Webpack’s ability to select from a variety of file extensions at build time makes it an especially useful tool for this task. You’d probably be surprised to see how just easy it is to achieve a high level of flexibility with minimal configuration.

More Intentional File Extensions

Part of Webpack’s magic comes from its ability to guess our intentions when we leave off the file extension at the end of an import. We do it so often we may barely notice, but how many times have we written code like this?

Maybe there’s a Nav.js file in the components directory, or possibly the extension is .jsx or .tsx. Maybe Nav is actually a directory itself, and there’s an index.js or index.jsx that’s exporting the component. Normally, it doesn’t much matter, unless we accidentally create two files with the same name and need to debug why one is given priority over the other. But there’s a hidden power in the extensions we normally omit out of convenience.

Create, Eject and Customize

To explore the negative space at the end of our import statements, let’s kick off a new React project with Create React App, and then quickly remove some of the generated boilerplate.

npx create-react-app whitelabelcd whitelabelyarn start

Once the browser window loads, you’ll see the familiar React logo and instructions to edit src/App.js. Let’s go ahead and change it to the following:

Finally, eject from Create React App’s warm embrace to expose the underlying Webpack configuration.

yarn eject

In the newly created config/paths.js, you will see the code that handles file extensions:

If you want to win an argument at the hotel bar of a React conference, then memorize this list. It’s the order in which Create React App will search for a matching file when you leave off the extension — which means that Nav.js will take priority over Nav.ts, but Nav.tsx gets priority over Nav.jsx. Who knew 🤷‍♂️?

But more importantly, now that we’ve ejected, we can strategically add custom extensions to the front of this list, giving us the ability to generate alternate versions of our app — for feature flagging, for A/B testing, or for building out entire white-label releases with different datasets, visual themes, or custom copy and markup!

As a simplified example, we can add the following code snippet below the declaration of moduleFileExtensions:

What’s happening here? We are effectively loading a copy of the default moduleFileExtentions before the original, each extension prefixed with an environment variable we can set at build time. So if we bundle our app with the VERSION variable set, files with the prefix will take priority over files without it. To test this proposition, create a new file in the src folder named App.special.js. Give it some slightly different markup so you will know it when you see its output:

Start the server once, to verify everything is the same as before. Then kill the server and start it again, this time setting the VERSION environment variable to special. You should see the “special” version of the app. Finally, restart it with a different value for the VERSION variable, and watch the app revert to its “generic” state.

yarn start
generic app
VERSION=special yarn start
special version
VERSION=whatever yarn start
generic app

What we’ve accomplished is nothing short of amazing! We can tell Webpack which files should be included in our build by matching file extensions to environment variables, and it will naturally fall back to its default behavior when no extension matches the version we are targeting.

Now that we’ve proven the concept, let’s do something interesting with it.

A Generic E-Commerce App

Let’s revisit our original example by building out a simple site with Nav and Main components. An online store is a good vehicle to demonstrate the kinds of customizations we can apply with this technique, since any store will want its own visual theme and copy, and of course its own list of products.

We can start by adding just a touch of global styling. We will use the Emotion library, but the same principles could be applied with any CSS-in-JS solution.

yarn add @emotion/core @emotion/babel-preset-css-prop

Because we’ve already ejected from Create React App, we can easily add the recommended Babel preset to the existing array towards the bottom of our package.json.

Now we can apply some base styles to src/App.js.

Notice that we import our values from a separate file, and interpolate those variables into our styles declaration. This is great for maintaining flexibility in any project, but essential for our ability to apply sitewide customizations later. Let’s create a src/styles.js file now and flesh it out a bit with some additional values. In a larger application, we would likely use TypeScript here to keep the shape of our styling objects consistent.

Now we can sketch out the markup and styling we will need to render the Nav component. Create a new file in src/components/Nav/index.js and add the following code:

Now we can import the Nav into src/App.js. Make sure you leave off the file extension!

Our Nav should now look something like this — rather “generic” indeed:

generic navbar

Let’s start to explore how we can use file extensions to customize the Nav component.

Specialized Styles

Because we’ve already colocated all of our sitewide styles into a single file, we can easily generate alternate themes by generating additional styles files with customized file extensions. For example, create a new file called src/styles.magic.js and add the following code:

Simply creating this file should have no effect on the application. But if we kill the server and run it again with the VERSION environment variable set to match our magic file extension, we’ll be able to see the custom styles we’ve applied.

VERSION=magic yarn start
magic navbar

Notice that the styles are being generated from styles.magic.js, but since there is no Nav.magic.js or App.magic.js, Webpack happily falls back to Nav.js and App.js from our generic application! This pattern works great for implementing a variety of visual themes, but we can extend it even further to customize our markup as well.

Custom Components

Let’s suppose we want different versions of our application to show customized links inside the Nav. Perhaps marketing is running an A/B test to see if one logo does better than another, or perhaps we want to support multiple stores and multiple brands. Either way, the implementation is fairly straightforward now that we have the proper patterns in place. Let’s start by breaking down the Nav component into its constituent parts:

Now we can implement each Nav link as its own component in its own file, giving us greater flexibility to customize the individual parts. Add the following code to src/components/Nav/Home.js:

And in src/components/Nav/Deals.js:

Finally, in src/components/Nav/Contact.js:

Once we’ve verified that everything is working the same as before, we can customize our Home component for the “magic” version of our app. Create a new file in src/components/Nav/Home.magic.js and add the following code.

If we run the app in magic mode, we will see our new markup, but notice that the new component only displays when the VERSION environment variable is set to magic. The generic version of our app is unaffected.

yarn start
generic navbar
VERSION=magic yarn start
home of magic navbar

Feature Flagged Files

Now let’s suppose we are working on a new version of our Deals component, but it’s not quite ready for production. We can use this same technique of matching file extensions with environment variables to boot our app into “beta” mode when we want to work on the new feature, without affecting what users see until we decide to roll it out. To demonstrate this, let’s create a new file at src/components/Nav/Deals.beta.js and add the following code:

Now we can run our app in beta mode and see the future feature:

VERSION=beta yarn start
free money

Whenever we are ready to unveil Deals 2.0 to the public, we can simply delete the original Deals.js and remove the beta from Deals.beta.js.

Opting Out

Imagine that down the road, we have a whole suite of stores we support: a magic store, a book store, a clothing store, and several others. The support team at the magic store is telepathic, and therefore wants to eliminate the Contact link, but the product owners at the other stores still require the feature. We can deprecate the Contact link for the magic store by creating a new file at src/components/Nav/Contact.magic.js and adding the following code:

Now the feature is effectively turned off at the magic store, but still active for any other stores we may be supporting.

no contact navbar

Generic Inventory

If the goal is in fact to support multiple stores, we are going to need a way for each store to display its own inventory. In a production environment we could stand up a headless CMS for each store; for the purposes of this demonstration, we can mimic that functionality by giving each store an Express API with some sample data on repl.it. The “generic” example is below.

We can create a new file at src/data.js to record the URL of our generic inventory API.

And now, we can consume this API from within our `Main` component. Create a new file at `src/components/Main/index.js` and add the following code:

We can complete our generic store by fleshing out the Product component. By keeping Product in its own file, we preserve the ability to easily customize it later if one brand wants to add a product description, or change the copy on the button. Place the following code in src/components/Main/Product.js:

Finally, import the Main component into src/App.js, leaving off the extension at the end of the filename.

The “generic” version of our store should look something like this in the browser:

generic inventory

Customized Datasets

Because we’ve been strategic about keeping all of the references to our data in a single file, we can easily swap out the entire dataset by standing up a “magic” version of our Express API:

And creating a new file at src/data.magic.js.

And just like that, we can restart the app in “magic” mode and see all of our customizations take effect!

VERSION=magic yarn start
magic inventory

Conclusion

We can see from these examples that this technique can be applied to a wide variety of problems, and no doubt there are many other applications beyond what we’ve explored. In fact, there’s nothing preventing us from tweaking our Webpack configuration further to support different combinations of file extensions — so we could in theory accommodate Deals.magic.beta.js, or Nav.magic.ios.js and Nav.magic.android.js. As the size of a project like this grows, you may find yourself using TypeScript to ensure data structures conform to consistent interfaces across different versions of the app.

Of course, not every project will require this sort of granular control over the bundling process, but it’s a powerful tool to have ready for the project that needs it. With a deeper understanding of a technology most of us happily gloss over, we can apply targeted tweaks to our build configuration and enable features and workflows that would otherwise be much more difficult to achieve.

--

--