Nx comes with dedicated documentation for each framework:

Migrating a Create-React-App project into an Nx Workspace

Create-React-App (CRA) is the most widely used tool for creating, building and testing a React app. This guide will show you how move an app generated with CRA into an Nx workspace. Once the migration process is complete, you'll be able to take advantage of all of Nx's features without needing to completely recreate your build process.

You can either use a CLI tool to migrate your app automatically, or you can follow the steps described below to do the migration manually.

Note: This guide has been updated for Nx 13 and may not work for earlier versions of Nx.

If you have a monorepo (more than one project in the same repo), follow the Adding Nx to Lerna/Yarn/PNPM/NPM Workspace guide instead.

Using a tool that will do it for you

You can use the cra-to-nx tool, that will run the following steps for you, and will turn your Create-React-App (CRA) project into an Nx workspace.

Just cd into your Create-React-App (CRA) project and run the following command:

npx cra-to-nx

Then just sit back and wait. After a while, take advantage of the full magic of Nx. Start from the commands mentioned in this article.

Note: The command will fail if you try execute it and you have uncommitted changes in your repository. Commit any local changes, and then try to run the command.

See it in action:

Doing the migration manually

In this article, you’ll learn how to:

  • Create an Nx workspace for a React application
  • Migrate a React application into your Nx workspace
  • Convert CRA scripts for use in Nx
  • Create a library and use it in your application

For this example, you’ll be migrating the default CRA typescript template app into an Nx workspace. This is the code that is generated when you run yarn create react-app webapp --template typescript.

There is also a repo that shows the finished result of this guide and for each step a diff will be provided to see the exact code changes that occur for that step.

1. Create your workspace

To start migrating your app, create an Nx workspace:

npx create-nx-workspace@latest acme --appName=webapp --preset=react --style=css --nx-cloud

Note: Replace acme with your organization's npm scope. This will be used when importing workspace projects. You can also replace webapp with a different name -- you can have more than one app in an Nx workspace.

2. Add npm packages to your workspace to support CRA

We'll need to add a few dependencies to the Nx workspace that are needed to allow CRA to function.

yarn add --dev react-scripts @testing-library/jest-dom eslint-config-react-app @craco/craco
yarn add web-vitals

# Or with npm
npm install --force --save-dev react-scripts @testing-library/jest-dom eslint-config-react-app @craco/craco
npm install --save web-vitals

Note: The @craco/craco package allows us to customize the webpack and jest config without ejecting.

3. Replace code generated by Nx with the CRA app

The source code for each app in an Nx workspace should be contained within the folder of a generated app. The create-nx-workspace command from step 1 created an app folder at apps/webapp that we can use to contain the CRA app. Delete the existing contents and copy over the CRA app code.

cd apps/webapp
ls -A . | grep -v 'project.json' | xargs  rm -rf
cd -
cp -r /path/to/cra-app/{README.md,package.json,tsconfig.json,src,public} apps/webapp

Replace /path/to/cra-app with the actual path to your CRA app on your machine.

4. Customize webpack using craco

The @craco/craco package allows you to customize the webpack config of CRA by creating apps/webapp/craco.config.js. Inline comments explain what each section is doing:

Copy this code if you are using CRA >=v5

1const path = require('path');
2const TsConfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
3const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
4module.exports = {
5  webpack: {
6    configure: (config) => {
7      // Remove guard against importing modules outside of \`src\`.
8      // Needed for workspace projects.
9      config.resolve.plugins = config.resolve.plugins.filter(
10        (plugin) => !(plugin instanceof ModuleScopePlugin)
11      );
12      // Add support for importing workspace projects.
13      config.resolve.plugins.push(
14        new TsConfigPathsPlugin({
15          configFile: path.resolve(__dirname, 'tsconfig.json'),
16          extensions: ['.ts', '.tsx', '.js', '.jsx'],
17          mainFields: ['module', 'main'],
18        })
19      );
20
21      // Replace include option for babel loader with exclude
22      // so babel will handle workspace projects as well.
23      config.module.rules[1].oneOf.forEach((r) => {
24        if (r.loader && r.loader.indexOf('babel') !== -1) {
25          r.exclude = /node_modules/;
26          delete r.include;
27        }
28      });
29
30      return config;
31    },
32  },
33  jest: {
34    configure: (config) => {
35      config.resolver = '@nrwl/jest/plugins/resolver';
36      return config;
37    },
38  },
39};

Copy this code if you are using CRA <=v4

1const path = require('path');
2const TsConfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
3const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
4module.exports = {
5  webpack: {
6    configure: (config) => {
7      // Remove guard against importing modules outside of \`src\`.
8      // Needed for workspace projects.
9      config.resolve.plugins = config.resolve.plugins.filter(
10        (plugin) => !(plugin instanceof ModuleScopePlugin)
11      );
12      // Add support for importing workspace projects.
13      config.resolve.plugins.push(
14        new TsConfigPathsPlugin({
15          configFile: path.resolve(__dirname, 'tsconfig.json'),
16          extensions: ['.ts', '.tsx', '.js', '.jsx'],
17          mainFields: ['module', 'main'],
18        })
19      );
20
21      // Replace include option for babel loader with exclude
22      // so babel will handle workspace projects as well.
23      config.module.rules.forEach((r) => {
24        if (r.oneOf) {
25          const babelLoader = r.oneOf.find(
26            (rr) => rr.loader.indexOf('babel-loader') !== -1
27          );
28          babelLoader.exclude = /node_modules/;
29          delete babelLoader.include;
30        }
31      });
32
33      return config;
34    },
35  },
36  jest: {
37    configure: (config) => {
38      config.resolver = '@nrwl/jest/plugins/resolver';
39      return config;
40    },
41  },
42};

5. Update package scripts

Update your app's package.json file (path: apps/webapp/package.json) to use craco instead of react-scripts.

{
  ...
  "scripts": {
    "serve": "craco start",
    "build": "BUILD_PATH=../../dist/apps/cra-app craco build",
    "test": "craco test",
    "eject": "react-scripts eject"
  },
  ...
}

Note: The BUILD_PATH variable is set to be consistent with other Nx projects. This is optional so you can remove it.

6. Remove targets from project.json

Your new project will use the craco scripts we added in apps/webapp/package.json in the previous step. Therefore, it will not need the targets set in project.json by Nx. In fact, if we leave the targets there, Nx will try to use this and it will fail. So we need to remove them.

In apps/webapp/project.json remove the targets object. The result will look like this:

{
  "root": "apps/webapp",
  "sourceRoot": "apps/webapp/src",
  "projectType": "application",
  "tags": []
}

7. Extend the app's tsconfig.json from the base

Modify apps/webapp/tsconfig.json to extend the root tsconfig.base.json. This is primarily to pickup the typescript aliases from the root tsconfig file.

1{
2  "extends": "../../tsconfig.base.json",
3  ...
4}

8. Add tsconfig files for jest and eslint

It's helpful to have separate tsconfig.json files for testing and linting. In this instance, the actual typescript settings are identical to the base config, so these tsconfig files will extend the base without modifying any values.

echo '{ "extends": "./tsconfig.json" }' > apps/webapp/tsconfig.app.json
echo '{ "extends": "./tsconfig.json" }' > apps/webapp/tsconfig.spec.json

9. Skip CRA preflight check since Nx manages the monorepo.

CRA checks to make sure there are no incompatible dependencies before any scripts run, but the @nrwl/react plugin serves the same purpose and requires slightly different versions in order to function correctly in an Nx workspace. Setting this environment variable disables CRA's check.

echo "SKIP_PREFLIGHT_CHECK=true" > .env

10. Add all node_modules to .gitignore

An apps/webapp/node_modules folder will be generated to hold some cache values when a build is run. This cache shouldn't be committed to git, so we tell git to ignore any node_modules folder.

echo "node_modules" >> .gitignore

Try Nx

1. Try the commands

The following commands are now available for you to try.

npx nx serve webapp
npx nx build webapp
npx nx test webapp

The serve command will automatically update when code changes, but needs to be restarted if you add a whole new library to your workspace.

build and test are set up to automatically cache their results. Subsequent runs of nx build webapp (without changing any code) should only take a couple seconds.

(No code changes for this step.)

2. Create a library

Nx makes it very easy to create isolated collections of reusable code in libraries. Running this script will create a library named ui-button.

nx generate lib ui-button

View the code changes

3. Use the library

The new library can be used in your app like by adding this code to App.tsx:

1//...
2import { UiButton } from '@acme/ui-button';
3//...
4<UiButton onClick={learnMore}>Learn React</UiButton>;
5//...

Make sure you also copy the code for your new ui-button from here.

The @acme/ui-button path alias is defined in the root tsconfig.base.json file.

Now serve the app again to see the result:

nx serve webapp

View the code changes

Summary

  • Create-React-App projects can be migrated into an Nx workspace using existing build and serve processes
  • @craco/craco allows you to continue using CRA and modify the webpack configuration
  • Caching is automatically enabled as part of the migration