Andrew James Frontend Engineer @ Coinbase

Creating multiple entry points in Create React App without ejecting

7 min read 2047

Creating Multiple Entry Points In Create-React-App Without Ejecting

Editor’s note: This article was last updated on 24 July 2023 to explain why multiple entry points are beneficial for modularity and scalability, and to add a section on why to to avoid ejecting.

I was recently tasked with building two applications in parallel. The first was a commercial web application, and the second acted as a platform to A/B test content messaging, page layouts, etc.

To be efficient, I wanted to reuse the majority of the core components and styles for both applications and interchange any branded assets (images, fonts, color palette, etc.) with a dummy brand using styled-components’ theming capabilities.

The challenge, then, was to create multiple applications from a single Create React App (CRA) application that shared common components and styles but with no trace of the other’s branded assets in their bundled build files. Thankfully, there are a number of ways to achieve this, ranging in complexity and development effort. In this article, we’ll explore why and how to add multiple entry points to a CRA application without ejecting.

Jump ahead:

Why add multiple entry points?

There are a number of benefits to adding multiple entry points to a React application:

  • Modularity: Multiple entry points allow you to break your application down into smaller, more manageable modules. This can make your application easier to understand, maintain, and test
  • Scalability: Multiple entry points can help you scale your application by allowing you to load different parts of the application on demand. This can improve performance and reduce the amount of memory that your application uses
  • Performance: Multiple entry points can improve the performance of your application by reducing the amount of code that needs to be loaded initially. This is especially beneficial for applications that have a lot of code or that are used on mobile devices
  • Security: Multiple entry points can help you improve the security of your application by allowing you to protect different parts of the application with different security settings. This can help you prevent unauthorized access to sensitive data

When to add multiple entry points in a React app

Not all React applications need multiple entry points. However, if your application meets any of the following criteria, you may want to consider adding multiple entry points:

  • Your application is large and complex
  • Your application is used on mobile devices
  • Your application needs to be scalable
  • You need to improve the performance of your application
  • You need to improve the security of your application

What is ejecting?

Ejecting is a process that allows you to customize the underlying webpack configuration of a CRA application. It removes the single build dependency from your project and copies all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc.) into your project as dependencies in package.json.

This can be useful if you need to make changes to the build process that are not supported by the default CRA configuration. However, ejecting also has some drawbacks, which we’ll discuss next.

Why to avoid ejecting a CRA app

Ejecting has some drawbacks that you should consider before doing it:

  • Irreversible: Once you eject, you can’t go back to the default configuration of Create React App. You will lose the benefits of having a single and consistent build dependency that is tested and updated regularly by the CRA team. You will have to maintain and update the configuration files and dependencies by yourself, which can be tedious and cause errors
  • Complex and time-consuming: Ejecting exposes you to complexity and details that CRA abstracts away for you. You will have to deal with low-level configuration options and plugins that may be unfamiliar or confusing to you. You will also have to learn how to use and integrate different tools and libraries that CRA uses under the hood, such as webpack, Babel, ESLint, etc. This is also a time-consuming process that can cause errors
  • May break some features: Ejecting may break some features or assumptions that Create React App relies on. For example, you may lose the ability to use some of the scripts or utilities that CRA provides, such as testing, debugging, or code splitting. You may also encounter some compatibility issues or conflicts with other tools or libraries that you want to use in your project
  • Unnecessary: Ejecting is not necessary for most React apps, and you can achieve a lot of customization and optimization without ejecting. Tools like react-app-rewired and customize-cra can be used to override or extend the default configuration of Create React App without ejecting. You can also use environment variables, proxy settings, or custom templates to modify some aspects of your app without ejecting
  • Hard to debug: Ejecting can make it more difficult to debug your application, especially if you are unfamiliar with the configuration and tools that CRA uses. You may lose some of the features or integrations that CRA provides for debugging, such as source maps, error overlays, or hot module replacement. You also might have to deal with more errors or warnings that are caused by the configuration or dependencies that you have ejected, as well as spend more time and effort to find and fix the bugs in your application

Therefore, you should avoid ejecting unless you have a specific need or preference that cannot be satisfied by the default configuration of Create React App. Ejecting is not a requirement for building React apps, and it may cause more problems than it solves. You should only eject if you are confident and comfortable with managing your own build tooling.

Multiple options for multiple entry points

Lerna is a popular tool that maintains multiple packages under a single repository (commonly referred to as a monorepo). It achieves this by linking identical dependencies across its packages, with the ability to publish them either collectively or individually.

Lerna can allow us to create a package for each application, and one for the core components to share between them. This solves the use case, but requires us to redesign the codebase and increase the complexity of the build process. Given that there are no immediate plans to add any other containers to the codebase, and that the testing application will likely not be required beyond the initial development phases, we decided that the associated overhead for this scenario was overkill.

A leaner approach would be to rewire the codebase with react-app-rewired, which tweaks the CRA build scripts without having to eject. In our case, we would use rewired to alter the application’s entry point at build time.

A major drawback here is that in doing so, we’d break the guarantees that CRA provides by hiding the configuration files from us, and the software itself is only lightly maintained by the community at the time of writing (customize-cra is a popular package built on top of rewired that supports CRA v2). This solution could be viable on a personal project, but it wasn’t something we were willing to depend on for a commercial application.

Ejecting is a one-way operation that cannot be undone. It allows us complete control of the project’s infrastructure by converting the codebase into a standard React application at the cost of transferring the responsibility of maintaining the exposed configuration to our team. This option is viable in some scenarios, but it’s usually considered a last resort due to the increased complexity and associated maintenance cost.

Each of these — and plenty more — are all viable solutions that come with their own set of benefits and drawbacks. However, for this particular scenario, we wanted to investigate a simple solution that allows us to work from a single codebase, not rely on third-party dependencies, and not eject from the safety net of CRA.

Conditionally importing container files

Let’s look at the default entry point in a CRA application. The src/index.js file imports the App container and renders it inside the div#root element defined in public/index.html:

/* src/index.js */
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
/* public/index.html */
  <noscript>You need to enable JavaScript to run this app</noscript>
  <div id="root"></div>

We can introduce multiple points of entry by importing both containers into the index.js file and conditionally render them based on a constant variable. This allows us to switch between the containers, but it comes with a few caveats.

In order to switch between the builds, we’d need to manually update the isTestEnv variable. The variable always needs to be correctly set when each of the sites are deployed, otherwise the wrong code would be deployed to the production environment:

/* src/index.js */
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
import Test from './test';

const isTestEnv = true;

if (isTestEnv) {
        ReactDOM.render(<Test />, document.getElementById("root"));
} else {
        ReactDOM.render(<App />, document.getElementById("root"));

Let’s tighten this up by creating a .env file with a custom environment variable. Now we have the ability to choose the build target before running the local development script, and also permanently assign a value to each of our production environments:

/* .env */
/* index.js */
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
import { Test } from './Test'; 

if (process.env.REACT_APP_BUILD_TARGET === 'test')
        ReactDOM.render(<Test />, document.getElementById("root"));
} else {
        ReactDOM.render(<App />, document.getElementById("root"));

We used Netlify to create a production environment for each application. Both sites will be virtually identical. They’ll both point to the same GitHub repository and have master set as the production branch. The only difference will be their respective BUILD_TARGET environment variable: test is assigned to the testing site, and app for the main application:

The Test Production Environment On Netlify

The App Production Environment On Netlify


We now have two production environments with the correct build target and free from human error. All that’s left is to ensure that only the code from the defined container appears in the bundled build.

Due to the nature of tree shaking, all of the imported containers in the application’s current index.js file would appear in the production build files, regardless of our build target. To remedy this, we can use CommonJS to conditionally require the desired container based on the BUILD_TARGET environment variable:

/* index.js */
require(process.env.REACT_APP_BUILD_TARGET === "test" 
  ? "./test" 
  : "./app"

This works, but setting the environment variable to anything other than test will import the main application. We can fix this with an if/else statement and further refine the solution with ES6 dynamic imports.

The importBuildTarget() function below will return a promise for each entry point and a fallback error in the event that the specified build target is not found. Once the import promise has resolved, it will render the requested build target with none of the other entry point files in the bundled build:

/* index.js */
import React from "react"; 
import ReactDOM from "react-dom"; 

function importBuildTarget() { 
  if (process.env.REACT_APP_BUILD_TARGET === "app") { 
    return import("./app.js"); 
  } else if (process.env.REACT_APP_BUILD_TARGET === "test") { 
    return import("./test.js"); 
  } else { 
    return Promise.reject(
      new Error("No such build target: " + process.env.REACT_APP_BUILD_TARGET)

// Import the entry point and render it's default export 
importBuildTarget().then(({ default: Environment }) => 
      <Environment /> 
  , document.getElementById("root") 


You can create multiple entry points in a CRA application without ejecting by using an environment variable to conditionally import container files. Doing this prevents code from the other containers appearing in the desired bundled build.


Special thanks to Stephen Taylor and Robin Weston for their valuable input, and to Jonathan Hawkes for his solution to all build target files appearing in the bundled build. Like the article? Let me know on Twitter.

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    Add to your HTML:

    <script src=""></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Andrew James Frontend Engineer @ Coinbase

12 Replies to “Creating multiple entry points in Create React App without…”

  1. This could be used to dynamically import assets (images, logos, backgrounds, fonts..) and bundle only that are conditionally used? Or maybe there is a better way to do that? Thank you!

    I come from Angular 2-4 that has a angular-cli.json file, and you could define multiple asset paths to be included from a parameter input called “app” (in latest angular versions “project”) and in React, this is managed by Webpack but if you start with CRA and don’t want to eject.. it’s a bit cumbersome. And this could be a solution.

  2. You are right. I did some tries and tests, and seems that if you do an image or asset conditionally import goes well (it’s not included in the final bundle), but if you try to with a JS file, it’s including anyway to the bundle. So, I think this post needs to be updated..

  3. Thanks for this! I was under the impression that the dynamic import would tree-shake the unused imports.

    I should have some time next week to update the post

  4. Great concept. But as others have posted build process is including all files in project regardless of the dynamic import entry file. Have you found a workaround?

  5. I’m additionally having the issue that css files from the non-included containers are being bundled in, causing clashes between styles.

  6. Nice article Andrew.

    It led me to look into React.lazy as an alternative that renders a dynamic import as a regular component.

    I’m pretty sure the React.lazy approach would work here (wrapped in a React Suspense with fallback) and only bundle in the dynamic import. This will likely resolve the issue people are having in these comments where the bundle currently contains both files?

    I’m not sure if a code snippet will work in this comment, but I’ll try:

    const BuildTarget = React.lazy(() => import(`./${process.env.REACT_APP_BUILD_TARGET}`));


  7. Thanks a lot for this! I had been searching all day for a “monorep” solution. However, I was unable to get your final solution to work as I was hitting a compile error with Sass that I could not resolve. Instead I modified the last step to just use a switch statement:

    // To add a new app, import it here…
    import ExampleReactApp from “./apps/example-react-app/App”;
    import ImsApp from “./apps/ims/App”;

    // …then add a line to the switch statement below, ensuring the “name” exactly matches the entry in the .env file
    let app;
    switch (process.env.REACT_APP_BUILD_TARGET) {
    case “ims”:
    app = ;
    app = ;

    ReactDOM.render({app}, document.getElementById(“root”));

  8. thank you for the PR, does that mean when the name is explicit, the import can be target. Otherwise it’ll try to include all of them? I’ll give it a shot, but seems making perfect sense.

Leave a Reply