Multiple Entry Points in Create React App Without Ejecting

  • Publish Date
  • Reading Time
    6 minutes
  • Tags
    • react
    • infra
Post

    I was recently tasked with building two applications in parallel. The first was a commercial web application, and a second that acted as a platform to A|B test content messaging, page layouts, and so on. To be ruthlessly efficient we wanted to reuse the majority of core components and styles for both applications, and interchange any branded assets (images, fonts, colour 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 henceforth) with each having no trace of the other's branded assets in their bundled build files. Thankfully there's a number of ways to achieve this, ranging in complexity and development effort.

    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 it's packages, with the ability to publish them either collectively or individually. Lerna would allow us to create a package for each application and one for the core components to share between them. This certainly solves the use case, but requires us to rearchitect the entire 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 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 (customise-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 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 were keen to investigate a simple solution that allows us to work from a single codebase, not rely on third-party dependancies, and not eject from the safety net of Create React App.

    To infinity, or beyond

    Let's look at the default entry point in a Create React Application. The

    Loading...
    file imports the
    Loading...
    container and renders it inside the
    Loading...
    element defined in
    Loading...
    .

    Loading...
    Loading...

    We can introduce multiple points of entry by importing both containers into the

    Loading...
    file and conditionally render them based on a constant variable. This allows us to switch between the containers, but comes with a couple of caveats. In order to switch between the builds we'd need to manually update the
    Loading...
    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.

    Loading...

    Let's tighten this up by creating a

    Loading...
    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.

    Loading...

    Enjoying the article?

    Support the content
    Loading...

    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

    Loading...
    environment variable:
    Loading...
    is assigned to the testing site, and
    Loading...
    for the main application.

    Netlify: App test environment
    Netlify: App test environment

    Netlify: App production environment
    Netlify: App production environment

    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

    Loading...
    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
    Loading...
    environment variable.

    Loading...

    This works, but setting the environment variable to anything other than

    Loading...
    will import the main application. We can fix this with an
    Loading...
    statement, and further refine the solution with ES6 dynamic imports. The
    Loading...
    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
    Loading...
    promise has resolved it will render the requested build target with none of the other entry point files in the bundled build. 💥

    Loading...

    TL; DR

    You can create multiple entry points in a Create React 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.

    Resources

    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 bundle.