Building a Single React App with Nx

In this tutorial you'll learn how to use React with Nx in a "standalone" (non-monorepo) setup.

What will you learn?

  • how to add Nx to a React and Vite project
  • how to run a single task (i.e. serve your app) or run multiple tasks in parallel
  • how to leverage code generators to scaffold components
  • how to modularize your codebase and impose architectural constraints for better maintainability
  • how to speed up CI with Nx Cloud ⚡
Looking for React monorepos?

Note, this tutorial sets up a repo with a single application at the root level that breaks out its code into libraries to add structure. If you are looking for a React monorepo setup then check out our React monorepo tutorial.

We're going to start with a default React application and progressively add the core of Nx, then use the @nx/react plugin. Visit our "Why Nx" page to learn more about plugins and what role they play in the Nx architecture.

Final Code

Here's the source code of the final result for this tutorial.

Creating a new React App

Create a new React application that uses Vite with the following command:

~

npm create vite react-app -- --template=react-ts

1 2Scaffolding project in ~/react-app... 3 4Done. Now run: 5 6 cd react-app 7 npm install 8 npm run dev 9

Once you have run npm install, set up Git with the following commands:

git init

git add .

git commit -m "initial commit"

Your repository should now have the following structure:

1└─ react-app 2 ├─ ... 3 ├─ public 4 │ └─ ... 5 ├─ src 6 │ ├─ assets 7 │ ├─ App.css 8 │ ├─ App.tsx 9 │ ├─ index.css 10 │ └─ main.tsx 11 ├─ .eslintrc.cjs 12 ├─ index.html 13 ├─ package.json 14 ├─ README.md 15 ├─ tsconfig.json 16 ├─ tsconfig.node.json 17 └─ vite.config.ts 18

The setup includes..

  • a new React application at the root of the repository (src)
  • ESLint preconfigured
  • Vite preconfigured

You can build the application with the following command:

1npm run build 2

Add Nx

Nx offers many features, but at its core, it is a task runner. Out of the box, it can:

After the initial set up, you can incrementally add on other features that would be helpful in your organization.

To enable Nx in your repository, run a single command:

npx nx@latest init

This command will download the latest version of Nx and help set up your repository to take advantage of it.

First, the script will propose installing some plugins based on the packages that are being used in your repository.

  • Leave the plugins deselected so that we can explore what Nx provides without any plugins.

Second, the script asks a series of questions to help set up caching for you.

  • Which scripts are cacheable? - Choose build and lint
  • Does the "build" script create any outputs? - Enter dist
  • Does the "lint" script create any outputs? - Enter nothing
  • Would you like remote caching to make your build faster? - Choose Skip for now

We'll enable Nx Cloud and set up remote caching later in the tutorial.

Caching Pre-configured

Nx has been configured to run your npm scripts as Nx tasks. You can run a single task like this:

npx nx build react-app

During the init script, Nx also configured caching for these tasks. You can see in the nx.json file that the build and lint targets have the cache property set to true and the build target specifies that its output goes to the project's dist folder.

nx.json
1{ 2 "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 "targetDefaults": { 4 "build": { 5 "outputs": ["{projectRoot}/dist"], 6 "cache": true 7 }, 8 "lint": { 9 "cache": true 10 } 11 }, 12 "defaultBase": "main" 13} 14

Try running build for the react-app app a second time:

npx nx build react-app

The first time nx build was run, it took about 2 seconds - just like running npm run build. But the second time you run nx build, it completes instantly and displays this message:

1Nx read the output from the cache instead of running the command for 1 out of 1 tasks. 2

You can see the same caching behavior working when you run npx nx lint.

Create a Task Pipeline

If you look at the build script in package.json, you'll notice that it is actually doing two things. First, it runs tsc to type check the application and then it uses Vite to build the application.

package.json
1{ 2 "scripts": { 3 "build": "tsc && vite build" 4 } 5} 6

Let's split this into two separate tasks, so we can run typecheck without running the build task.

package.json
1{ 2 "scripts": { 3 "typecheck": "tsc", 4 "build": "vite build" 5 } 6} 7

But we also want to make sure that typecheck is always run when you run the build task. Nx can take care of this for you with a task pipeline. The dependsOn property in nx.json can be used to ensure that all task dependencies are run first.

nx.json
1{ 2 "targetDefaults": { 3 "build": { 4 "outputs": ["{projectRoot}/dist"], 5 "cache": true, 6 "dependsOn": ["typecheck"] 7 }, 8 "lint": { 9 "cache": true 10 } 11 } 12} 13
Project-Specific Settings

The targetDefaults in the nx.json file will apply to all the projects in your repository. If you want to specify these options for a specific project, you can set them under nx.targets.build in that project's package.json file.

Now if you run nx build, Nx will run typecheck first.

~/react-app

npx nx build

1> nx run react-app:typecheck 2 3 4> react-app@0.0.0 typecheck 5> tsc 6 7 8> nx run react-app:build 9 10 11> react-app@0.0.0 build 12> vite build 13 14vite v5.2.12 building for production... 15✓ 34 modules transformed. 16dist/index.html 0.46 kB │ gzip: 0.30 kB 17dist/assets/react-CHdo91hT.svg 4.13 kB │ gzip: 2.05 kB 18dist/assets/index-DiwrgTda.css 1.39 kB │ gzip: 0.72 kB 19dist/assets/index-DVoHNO1Y.js 143.36 kB │ gzip: 46.09 kB 20✓ built in 347ms 21 22—————————————————————————————————————————————————————— 23 24 NX Successfully ran target build for project react-app and 1 task it depends on (2s) 25

We can also cache the typecheck task by updating the nx.json file.

nx.json
1{ 2 "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 "targetDefaults": { 4 "typecheck": { 5 "cache": true 6 }, 7 "build": { 8 "outputs": ["{projectRoot}/dist"], 9 "cache": true, 10 "dependsOn": ["typecheck"] 11 }, 12 "lint": { 13 "cache": true 14 } 15 }, 16 "defaultBase": "main" 17} 18

Now, running npx nx build twice will once again complete instantly.

Use Nx Plugins to Enhance Vite Tasks with Caching

You may remember that we defined the outputs property in nx.json when we were answering questions in the nx init script. The value is currently hard-coded so that if you change the output path in your vite.config.ts, you have to remember to also change the outputs array in the build task configuration. This is where plugins can help. Plugins enable better integration with specific tools. The @nx/vite plugin can understand the vite.config.ts file and automatically create and configure tasks based on the settings in that file.

Nx plugins can:

  • automatically configure caching for you, including inputs and outputs based on the underlying tooling configuration
  • create tasks for a project using the tooling configuration files
  • provide code generators to help scaffold out projects
  • automatically keep the tooling versions and configuration files up to date

For this tutorial, we'll just focus on the automatic caching configuration.

First, let's delete the outputs array from nx.json so that we don't override the inferred values from the plugin. Your nx.json should look like this:

nx.json
1{ 2 "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 "targetDefaults": { 4 "typecheck": { 5 "cache": true 6 }, 7 "build": { 8 "cache": true, 9 "dependsOn": ["typecheck"] 10 }, 11 "lint": { 12 "cache": true 13 } 14 }, 15 "defaultBase": "main" 16} 17

Now let's add the @nx/vite plugin:

~/react-app

npx nx add @nx/vite

1✔ Installing @nx/vite... 2✔ Initializing @nx/vite... 3 4 NX Package @nx/vite added successfully. 5

The nx add command installs the version of the plugin that matches your repo's Nx version and runs that plugin's initialization script. For @nx/vite, the initialization script registers the plugin in the plugins array of nx.json and updates any package.json scripts that execute Vite related tasks to run those tasks through Nx. Running the tasks through Nx is necessary for caching and task pipelines to work.

Open the project details view for the demo app and look at the build task. You can view the project details using Nx Console or by running the following command in the terminal:

npx nx show project react-app

Loading...

If you hover over the settings for the build task, you can see where those settings come from. The inputs and outputs are defined by the @nx/vite plugin from the vite.config.ts file where as the dependsOn property we set earlier in the tutorial in the targetDefaults in the nx.json file.

Now let's change where the build results are output to in the vite.config.ts file.

vite.config.ts
1import { defineConfig } from 'vite'; 2import react from '@vitejs/plugin-react'; 3 4// https://vitejs.dev/config/ 5export default defineConfig({ 6 plugins: [react()], 7 build: { 8 outDir: 'dist/react-app', 9 }, 10}); 11

Now if you look at project details view again, you'll see that the outputs property for Nx's caching has been updated to stay in sync with the setting in the vite.config.ts file.

Creating New Components

You can just create new React components as you normally would. However, Nx plugins also ship generators. They allow you to easily scaffold code, configuration or entire projects. Let's add the @nx/react plugin to take advantage of the generators it provides.

npx nx add @nx/react

To see what capabilities the @nx/react plugin ships, run the following command and inspect the output:

react-app

npx nx list @nx/react

1 NX Capabilities in @nx/react: 2 3GENERATORS 4 5init : Initialize the `@nx/react` plugin. 6application : Create a React application. 7library : Create a React library. 8component : Create a React component. 9redux : Create a Redux slice for a project. 10storybook-configuration : Set up storybook for a React app or library. 11component-story : Generate storybook story for a React component 12stories : Create stories/specs for all components declared in an app or library. 13component-cypress-spec : Create a Cypress spec for a UI component that has a story. 14hook : Create a hook. 15host : Generate a host react application 16remote : Generate a remote react application 17cypress-component-configuration : Setup Cypress component testing for a React project 18component-test : Generate a Cypress component test for a React component 19setup-tailwind : Set up Tailwind configuration for a project. 20setup-ssr : Set up SSR configuration for a project. 21federate-module : Federate a module. 22 23EXECUTORS/BUILDERS 24 25module-federation-dev-server : Serve a host or remote application. 26module-federation-ssr-dev-server : Serve a host application along with it's known remotes. 27
Integrate with Your Editor

For a more integrated experience, install the "Nx Console" extension for your code editor. It has support for VSCode, IntelliJ and ships a LSP for Vim. Nx Console provides autocompletion support in Nx configuration files and has UIs for browsing and running generators.

More info can be found in the integrate with editors article.

Run the following command to generate a new "hello-world" component. Note how we append --dry-run to first check the output.

react-app

npx nx g @nx/react:component --directory=src/app/hello-world --skipTests=true hello-world --dry-run

1NX Generating @nx/react:component 2 3✔ Which stylesheet format would you like to use? · css 4✔ Should this component be exported in the project? (y/N) · false 5✔ Where should the component be generated? · src/app/hello-world/hello-world.tsx 6CREATE src/app/hello-world/hello-world.module.css 7CREATE src/app/hello-world/hello-world.tsx 8 9NOTE: The "dryRun" flag means no changes were made. 10

As you can see it generates a new component in the src/app/hello-world/ folder. If you want to actually run the generator, remove the --dry-run flag.

The hello-world component will look like this:

src/app/hello-world/hello-world.tsx
1import styles from './hello-world.module.css'; 2 3export function HelloWorld() { 4 return ( 5 <div className={styles['container']}> 6 <h1>Welcome to HelloWorld!</h1> 7 </div> 8 ); 9} 10 11export default HelloWorld; 12

Let's update the App.tsx file to use the new HelloWorld component:

src/App.tsx
1import './App.css'; 2import HelloWorld from './app/hello-world/hello-world'; 3 4function App() { 5 return ( 6 <> 7 <HelloWorld /> 8 </> 9 ); 10} 11 12export default App; 13

You can view your app with the nx serve command:

npx nx serve

You're ready to go!

In the previous sections you learned about the basics of using Nx, running tasks and navigating an Nx workspace. You're ready to ship features now!

But there's more to learn. You have two possibilities here:

Modularize your React App with Local Libraries

When you develop your React application, usually all your logic sits in the src folder. Ideally separated by various folder names which represent your "domains". As your app grows, this becomes more and more monolithic though.

1└─ react-app 2 ├─ ... 3 ├─ src 4 │ ├─ app 5 │ │ ├─ products 6 │ │ ├─ cart 7 │ │ ├─ ui 8 │ │ ├─ ... 9 │ │ └─ app.tsx 10 │ ├─ ... 11 │ └─ main.tsx 12 ├─ ... 13 ├─ package.json 14 ├─ ... 15

Nx allows you to separate this logic into "local libraries". The main benefits include

  • better separation of concerns
  • better reusability
  • more explicit "APIs" between your "domain areas"
  • better scalability in CI by enabling independent test/lint/build commands for each library
  • better scalability in your teams by allowing different teams to work on separate libraries

Create a Local Library

Let's assume our domain areas include products, orders and some more generic design system components, called ui. We can generate a new library for these areas using the React library generator:

1npx nx g @nx/react:library products --unitTestRunner=vitest --bundler=none --directory=modules/products 2

Note how we use the --directory flag to place the library into a subfolder. You can choose whatever folder structure you like to organize your libraries.

Nx sets up your workspace to work with the modular library architecture, but depending on your existing configuration, you may need to tweak some settings. In this repo, you'll need to make a small change in order to prepare for future steps.

Build Settings

To make sure that the build can correctly pull in code from libraries, we'll update vite.config.ts to account for typescript aliases. Run the following generator to automatically update your configuration file.

npx nx g @nx/vite:setup-paths-plugin

This will update the vite.config.ts file to include the nxViteTsPaths plugin in the plugins array.

vite.config.ts
1import { defineConfig } from 'vite'; 2import react from '@vitejs/plugin-react'; 3import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 4 5// https://vitejs.dev/config/ 6export default defineConfig({ 7 plugins: [react(), nxViteTsPaths()], 8 build: { 9 outDir: 'dist/react-app', 10 }, 11}); 12

Create More Libraries

Now that the build system is set up, let's generate the orders and ui libraries.

1nx g @nx/react:library orders --unitTestRunner=vitest --bundler=none --directory=modules/orders 2nx g @nx/react:library ui --unitTestRunner=vitest --bundler=none --directory=modules/shared/ui 3

Running the above commands should lead to the following directory structure:

1└─ react-app 2 ├─ ... 3 ├─ modules 4 │ ├─ products 5 │ │ ├─ ... 6 │ │ ├─ project.json 7 │ │ ├─ src 8 │ │ │ ├─ index.ts 9 │ │ │ └─ lib 10 │ │ │ ├─ products.spec.ts 11 │ │ │ └─ products.ts 12 │ │ ├─ tsconfig.json 13 │ │ ├─ tsconfig.lib.json 14 │ │ ├─ tsconfig.spec.json 15 │ │ └─ vite.config.ts 16 │ ├─ orders 17 │ │ ├─ ... 18 │ │ ├─ project.json 19 │ │ ├─ src 20 │ │ │ ├─ index.ts 21 │ │ │ └─ ... 22 │ │ └─ ... 23 │ └─ shared 24 │ └─ ui 25 │ ├─ ... 26 │ ├─ project.json 27 │ ├─ src 28 │ │ ├─ index.ts 29 │ │ └─ ... 30 │ └─ ... 31 ├─ src 32 │ ├─ app 33 │ │ ├─ hello-world 34 │ │ │ ├─ hello-world.module.css 35 │ │ │ └─ hello-world.tsx 36 │ │ └─ ... 37 │ ├─ ... 38 │ └─ main.tsx 39 ├─ ... 40

Each of these libraries

  • has a project details view where you can see the available tasks (e.g. running tests for just orders: nx test orders)
  • has its own project.json file where you can customize targets
  • has a dedicated index.ts file which is the "public API" of the library
  • is mapped in the tsconfig.base.json at the root of the workspace

Importing Libraries into the React Application

All libraries that we generate automatically have aliases created in the root-level tsconfig.base.json.

tsconfig.base.json
1{ 2 "compilerOptions": { 3 ... 4 "paths": { 5 "orders": ["modules/orders/src/index.ts"], 6 "products": ["modules/products/src/index.ts"], 7 "ui": ["modules/shared/ui/src/index.ts"] 8 }, 9 ... 10 }, 11} 12

That way we can easily import them into other libraries and our React application. As an example, let's import the Products component from the products project into our main application. First (if you haven't already), let's set up React Router.

npm add react-router-dom

Configure it in the main.tsx.

src/main.tsx
1import { StrictMode } from 'react'; 2import { BrowserRouter } from 'react-router-dom'; 3import ReactDOM from 'react-dom/client'; 4 5import App from './App'; 6 7const root = ReactDOM.createRoot( 8 document.getElementById('root') as HTMLElement 9); 10 11root.render( 12 <StrictMode> 13 <BrowserRouter> 14 <App /> 15 </BrowserRouter> 16 </StrictMode> 17); 18

Then we can import the Products component into our app.tsx and render it via the routing mechanism whenever a user hits the /products route.

src/App.tsx
1import './App.css'; 2import HelloWorld from './app/hello-world/hello-world'; 3import { Route, Routes } from 'react-router-dom'; 4 5// importing the component from the library 6import { Products } from 'products'; 7 8export function App() { 9 return ( 10 <Routes> 11 <Route path="/" element={<HelloWorld />}></Route> 12 <Route path="/products" element={<Products />}></Route> 13 </Routes> 14 ); 15} 16 17export default App; 18

Serving your app (nx serve) and then navigating to /products should give you the following result:

products route

Let's apply the same steps for our orders library. Import the Orders component into the App.tsx file and render it via the routing mechanism whenever a user hits the /orders route

In the end, your App.tsx should look similar to this:

src/App.tsx
1import './App.css'; 2import HelloWorld from './app/hello-world/hello-world'; 3import { Route, Routes } from 'react-router-dom'; 4import { Products } from 'products'; 5import { Orders } from 'orders'; 6 7export function App() { 8 return ( 9 <Routes> 10 <Route path="/" element={<HelloWorld />}></Route> 11 <Route path="/products" element={<Products />}></Route> 12 <Route path="/orders" element={<Orders />}></Route> 13 </Routes> 14 ); 15} 16 17export default App; 18

Visualizing your Project Structure

Nx automatically detects the dependencies between the various parts of your workspace and builds a project graph. This graph is used by Nx to perform various optimizations such as determining the correct order of execution when running tasks like nx build, identifying affected projects and more. Interestingly you can also visualize it.

Just run:

nx graph

You should be able to see something similar to the following in your browser.

Loading...

Notice how ui is not yet connected to anything because we didn't import it in any of our projects.

Exercise for you: change the codebase such that ui is used by orders and products.

Imposing Constraints with Module Boundary Rules

Once you modularize your codebase you want to make sure that the modules are not coupled to each other in an uncontrolled way. Here are some examples of how we might want to guard our small demo workspace:

  • we might want to allow orders to import from ui but not the other way around
  • we might want to allow orders to import from products but not the other way around
  • we might want to allow all libraries to import the ui components, but not the other way around

When building these kinds of constraints you usually have two dimensions:

  • type of project: what is the type of your library. Example: "feature" library, "utility" library, "data-access" library, "ui" library
  • scope (domain) of the project: what domain area is covered by the project. Example: "orders", "products", "shared" ... this really depends on the type of product you're developing

Nx comes with a generic mechanism that allows you to assign "tags" to projects. "tags" are arbitrary strings you can assign to a project that can be used later when defining boundaries between projects. For example, go to the project.json of your orders library and assign the tags type:feature and scope:orders to it.

modules/orders/project.json
1{ 2 ... 3 "tags": ["type:feature", "scope:orders"] 4} 5

Then go to the project.json of your products library and assign the tags type:feature and scope:products to it.

modules/products/project.json
1{ 2 ... 3 "tags": ["type:feature", "scope:products"] 4} 5

Finally, go to the project.json of the ui library and assign the tags type:ui and scope:shared to it.

modules/shared/ui/project.json
1{ 2 ... 3 "tags": ["type:ui", "scope:shared"] 4} 5

Notice how we assign scope:shared to our UI library because it is intended to be used throughout the workspace.

Next, let's come up with a set of rules based on these tags:

  • type:feature should be able to import from type:feature and type:ui
  • type:ui should only be able to import from type:ui
  • scope:orders should be able to import from scope:orders, scope:shared and scope:products
  • scope:products should be able to import from scope:products and scope:shared

To enforce the rules, Nx ships with a custom ESLint rule.

Lint Settings

We want the lint task for the root react-app project to only lint the files for that project (in the src folder), so we'll change the lint command in package.json:

package.json
1{ 2 "scripts": { 3 "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0" 4 } 5} 6

Install the @nx/eslint-plugin package. This is an ESLint plugin that can be used in the plugins property of your ESLint configuration. There is also an Nx plugin named @nx/eslint that was automatically installed with the @nx/react plugin that was added earlier in the tutorial.

1npx nx add @nx/eslint-plugin 2

We need to update the .eslintrc.cjs file to extend the .eslintrc.base.json file and undo the ignorePattern from that config that ignores every file. The .eslintrc.base.json file serves as a common set of lint rules for every project in the repository.

.eslintrc.cjs
1module.exports = { 2 root: true, 3 env: { browser: true, es2020: true }, 4 extends: [ 5 'eslint:recommended', 6 'plugin:@typescript-eslint/recommended', 7 'plugin:react-hooks/recommended', 8 './.eslintrc.base.json', 9 ], 10 ignorePatterns: ['!**/*', 'dist', '.eslintrc.cjs'], 11 parser: '@typescript-eslint/parser', 12 plugins: ['react-refresh'], 13 rules: { 14 'react-refresh/only-export-components': [ 15 'warn', 16 { allowConstantExport: true }, 17 ], 18 }, 19}; 20

Now we need to update the .eslintrc.base.json file and define the depConstraints in the @nx/enforce-module-boundaries rule:

.eslintrc.base.json
1{ 2 "overrides": [ 3 { 4 "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 5 "rules": { 6 "@nx/enforce-module-boundaries": [ 7 "error", 8 { 9 "enforceBuildableLibDependency": true, 10 "allow": [], 11 "depConstraints": [ 12 { 13 "sourceTag": "*", 14 "onlyDependOnLibsWithTags": ["*"] 15 }, 16 { 17 "sourceTag": "type:feature", 18 "onlyDependOnLibsWithTags": ["type:feature", "type:ui"] 19 }, 20 { 21 "sourceTag": "type:ui", 22 "onlyDependOnLibsWithTags": ["type:ui"] 23 }, 24 { 25 "sourceTag": "scope:orders", 26 "onlyDependOnLibsWithTags": [ 27 "scope:orders", 28 "scope:products", 29 "scope:shared" 30 ] 31 }, 32 { 33 "sourceTag": "scope:products", 34 "onlyDependOnLibsWithTags": ["scope:products", "scope:shared"] 35 }, 36 { 37 "sourceTag": "scope:shared", 38 "onlyDependOnLibsWithTags": ["scope:shared"] 39 } 40 ] 41 } 42 ] 43 } 44 } 45 ... 46 ] 47} 48

When Nx set up the @nx/eslint plugin, it chose a task name that would not conflict with the pre-existing lint script. Let's overwrite that name so that all the linting tasks use the same lint name. Update the setting in the nx.json file:

nx.json
1{ 2 ... 3 "plugins": [ 4 { 5 "plugin": "@nx/eslint/plugin", 6 "options": { 7 "targetName": "lint" 8 } 9 } 10 ] 11} 12

Test Boundary Rules

To test the boundary rules, go to your modules/products/src/lib/products.tsx file and import the Orders from the orders project:

modules/products/src/lib/products.tsx
1import styles from './products.module.css'; 2 3// This import is not allowed 👇 4import { Orders } from 'orders'; 5 6/* eslint-disable-next-line */ 7export interface ProductsProps {} 8 9export function Products() { 10 return ( 11 <div className={styles['container']}> 12 <h1>Welcome to Products!</h1> 13 </div> 14 ); 15} 16 17export default Products; 18

If you lint your workspace you'll get an error now:

nx run-many -t lint

1✔ nx run orders:lint [existing outputs match the cache, left as is] 2✔ nx run ui:lint (1s) 3 4✖ nx run products:lint 5 Linting "products"... 6 7 /Users/.../react-app/modules/products/src/lib/products.tsx 8 3:1 error A project tagged with "scope:products" can only depend on libs tagged with "scope:products", "scope:shared" @nx/enforce-module-boundaries 9 101 problem (1 error, 0 warnings) 11 12 Lint errors found in the listed files. 13 14✔ nx run react-app:lint (1s) 15 16———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 17 18NX Ran target lint for 4 projects (1s) 19 203/4 succeeded [1 read from cache] 21 221/4 targets failed, including the following: 23 - nx run products:lint 24

If you have the ESLint plugin installed in your IDE you should also immediately see an error.

Learn more about how to enforce module boundaries.

Migrating to a Monorepo

When you are ready to add another application to the repo, you'll probably want to move react-app to its own folder. To do this, you can run the convert-to-monorepo generator or manually move the configuration files.

You can also go through the full React monorepo tutorial

Fast CI ⚡

Repository with Nx

Make sure you have completed the previous sections of this tutorial before starting this one. If you want a clean starting point, you can check out the reference code as a starting point.

This tutorial walked you through how Nx can improve the local development experience, but the biggest difference Nx makes is in CI. As repositories get bigger, making sure that the CI is fast, reliable and maintainable can get very challenging. Nx provides a solution.

Connect to Nx Cloud

Nx Cloud is a companion app for your CI system that provides remote caching, task distribution, e2e tests deflaking, better DX and more.

Now that we're working on the CI pipeline, it is important for your changes to be pushed to a GitHub repository.

  1. Commit your existing changes with git add . && git commit -am "updates"
  2. Create a new GitHub repository
  3. Follow GitHub's instructions to push your existing code to the repository

Now connect your repository to Nx Cloud with the following command:

npx nx connect

A browser window will open to register your repository in your Nx Cloud account. The link is also printed to the terminal if the windows does not open, or you closed it before finishing the steps. The app will guide you to create a PR to enable Nx Cloud on your repository.

Once the PR is created, merge it into your main branch.

And make sure you pull the latest changes locally:

git pull

You should now have an nxCloudId property specified in the nx.json file.

Create a CI Workflow

Use the following command to generate a CI workflow file.

npx nx generate ci-workflow --ci=github

This generator creates a .github/workflows/ci.yml file that contains a CI pipeline that will run the lint, test, build and e2e tasks for projects that are affected by any given PR. Since we are using Nx Cloud, the pipeline will also distribute tasks across multiple machines to ensure fast and reliable CI runs.

The key lines in the CI pipeline are:

.github/workflows/ci.yml
1name: CI 2# ... 3jobs: 4 main: 5 runs-on: ubuntu-latest 6 steps: 7 - uses: actions/checkout@v4 8 with: 9 fetch-depth: 0 10 # This enables task distribution via Nx Cloud 11 # Run this command as early as possible, before dependencies are installed 12 # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun 13 # Connect your workspace by running "nx connect" and uncomment this 14 - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build" 15 - uses: actions/setup-node@v3 16 with: 17 node-version: 20 18 cache: 'npm' 19 - run: npm ci --legacy-peer-deps 20 - uses: nrwl/nx-set-shas@v4 21 # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected 22 - run: npx nx affected -t lint test build 23

Open a Pull Request

Commit the changes and open a new PR on GitHub.

git add .

git commit -m 'add CI workflow file'

git push origin add-workflow

When you view the PR on GitHub, you will see a comment from Nx Cloud that reports on the status of the CI run.

Nx Cloud report

The See all runs link goes to a page with the progress and results of tasks that were run in the CI pipeline.

Run details

For more information about how Nx can improve your CI pipeline, check out one of these detailed tutorials:

Next Steps

Here's some things you can dive into next:

Also, make sure you