Building React Apps with the Nx Standalone Setup

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

What are you going to learn?

  • how to create a new React application
  • 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
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.

Note, while you could easily use Nx together with your manually set up React application, we're going to use the @nx/react plugin for this tutorial which provides some nice enhancements when working with React. Visit our "Why Nx" page to learn more about plugins and what role they play in the Nx architecture.

Warm Up

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

Creating a new React App

Create a new standalone React application with the following command:

~

npx create-nx-workspace@latest myreactapp --preset=react-standalone

1✔ Which bundler would you like to use? · vite 2Test runner to use for end to end (E2E) tests · cypress 3Default stylesheet format · css 4Set up CI with caching, distribution and test deflaking · github 5

You can choose any bundler you like. In this tutorial we're going to use Vite. The above command generates the following structure:

1└─ myreactapp 2 ├─ ... 3 ├─ e2e 4 │ └─ ... 5 ├─ public 6 │ └─ ... 7 ├─ src 8 │ ├─ app 9 │ │ ├─ app.module.css 10 │ │ ├─ app.spec.tsx 11 │ │ ├─ app.tsx 12 │ │ └─ nx-welcome.tsx 13 │ ├─ assets 14 │ ├─ main.tsx 15 │ └─ styles.css 16 ├─ index.html 17 ├─ nx.json 18 ├─ package.json 19 ├─ project.json 20 ├─ tsconfig.app.json 21 ├─ tsconfig.json 22 ├─ tsconfig.spec.json 23 └─ vite.config.ts 24

The setup includes..

  • a new React application at the root of the Nx workspace (src/app)
  • a Cypress based set of e2e tests (e2e/)
  • Prettier preconfigured
  • ESLint preconfigured
  • Jest preconfigured

Let me explain a couple of things that might be new to you.

FileDescription
nx.jsonThis is where we fine-tune how Nx works. We define what cacheable operations there are, and configure our task pipeline. More on that soon.
project.jsonThis file is where you can modify the inferred tasks for the myreactapp project. More about this later.

Serving the App

The most common tasks are already mapped in the package.json file:

package.json
1{ 2 "name": "myreactapp", 3 "scripts": { 4 "start": "nx serve", 5 "build": "nx build", 6 "test": "nx test" 7 } 8 ... 9} 10

To serve your new React application, just run: npm start. Alternatively you can directly use Nx by using

nx serve

Your application should be served at http://localhost:4200.

Nx uses the following syntax to run tasks:

Syntax for Running Tasks in Nx

Inferred Tasks

Nx identifies available tasks for your project from tooling configuration files, package.json scripts and the targets defined in project.json. To view the tasks that Nx has detected, look in the Nx Console project detail view or run:

nx show project myreactapp --web

Project Details View (Simplified)

myreactapp

Root: .

Type: Application

Targets

  • build

    vite build

    Cacheable

If you expand the build task, you can see that it was created by the @nx/vite plugin by analyzing your vite.config.ts file. Notice the outputs are defined as {projectRoot}/dist/myreactapp. This value is being read from the build.outDir defined in your vite.config.ts file. Let's change that value in your vite.config.ts file:

vite.config.ts
1export default defineConfig({ 2 // ... 3 build: { 4 outDir: './build/myreactapp', 5 // ... 6 }, 7}); 8

Now if you look at the project details view, the outputs for the build target will say {projectRoot}/build/myreactapp. This feature ensures that Nx will always cache the correct files.

You can also override the settings for inferred tasks by modifying the targetDefaults in nx.json or setting a value in your project.json file. Nx will merge the values from the inferred tasks with the values you define in targetDefaults and in your specific project's configuration.

Testing and Linting - Running Multiple Tasks

Our current setup doesn't just come with targets for serving and building the React application, but also has targets for unit testing, e2e testing and linting. We can use the same syntax as before to run these tasks:

1nx test # runs tests using Vitest (or you can configure it to use Jest) 2nx lint # runs linting with ESLint 3nx e2e e2e # runs e2e tests with Cypress 4

More conveniently, we can also run them in parallel using the following syntax:

myreactapp

nx run-many -t test lint e2e

1 2nx run e2e:lint (2s) 3nx run myreactapp:lint (2s) 4nx run myreactapp:test (2s) 5nx run e2e:e2e (6s) 6 7 —————————————————————————————————————————————————————— 8 9 > NX Successfully ran targets test, lint, e2e for 2 projects (7s) 10

Caching

One thing to highlight is that Nx is able to cache the tasks you run.

Note that all of these targets are automatically cached by Nx. If you re-run a single one or all of them again, you'll see that the task completes immediately. In addition, (as can be seen in the output example below) there will be a note that a matching cache result was found and therefore the task was not run again.

myreactapp

nx run-many -t test lint e2e

1 2 ✔ nx run e2e:lint [existing outputs match the cache, left as is] 3 ✔ nx run myreactapp:lint [existing outputs match the cache, left as is] 4 ✔ nx run myreactapp:test [existing outputs match the cache, left as is] 5 ✔ nx run e2e:e2e [existing outputs match the cache, left as is] 6 7 —————————————————————————————————————————————————————— 8 9 > NX Successfully ran targets test, lint, e2e for 5 projects (54ms) 10 11 Nx read the output from the cache instead of running the command for 10 out of 10 tasks. 12

Not all tasks might be cacheable though. You can configure the cache properties in the targets under targetDefaults in the nx.json file. You can also learn more about how caching works.

Nx Plugins? Why?

One thing you might be curious about is the inferred tasks. You may wonder why we are detecting tasks from your tooling configuration instead of directly defining them in package.json scripts or in the project.json file.

Nx understands and supports both approaches, allowing you to define tasks in your package.json and project.json files or have Nx plugins automatically detect them. The inferred tasks give you the benefit of automatically setting the Nx cache settings for you based on your tooling configuration. In this tutorial, we take advantage of those inferred tasks to demonstrate the full value of Nx plugins.

So, what are Nx Plugins? Nx Plugins are optional packages that extend the capabilities of Nx, catering to various specific technologies. For instance, we have plugins tailored to React (e.g., @nx/react), Vite (@nx/vite), Cypress (@nx/cypress), and more. These plugins offer additional features, making your development experience more efficient and enjoyable when working with specific tech stacks.

Visit our "Why Nx" page for more details.

Creating New Components

You can just create new React components as you normally would. However, Nx plugins usually also ship generators. They allow you to easily scaffold code, configuration or entire projects. To see what capabilities the @nx/react plugin ships, run the following command and inspect the output:

myreactapp

npx nx list @nx/react

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

If you prefer a more integrated experience, you can 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.

myreactapp

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

1> NX Generating @nx/react:component 2 3✔ Should this component be exported in the project? (y/N) · false 4✔ Where should the component be generated? · src/app/hello-world/hello-world.tsx 5CREATE src/app/hello-world/hello-world.module.css 6CREATE src/app/hello-world/hello-world.spec.tsx 7CREATE src/app/hello-world/hello-world.tsx 8 9NOTE: The "dryRun" flag means no changes were made. 10
Nx 15 and lower use @nrwl/ instead of @nx/

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

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

Building the App for Deployment

If you're ready and want to ship your application, you can build it using

myreactapp

npx nx build

1vite v4.3.5 building for production... 233 modules transformed. 3dist/myreactapp/index.html 0.48 kB │ gzip: 0.30 kB 4dist/myreactapp/assets/index-e3b0c442.css 0.00 kB │ gzip: 0.02 kB 5dist/myreactapp/assets/index-378e8124.js 165.64 kB │ gzip: 51.63 kB 6built in 496ms 7 8 —————————————————————————————————————————————————————————————————————————————————————————————————————————— 9 10 > NX Successfully ran target build for project reactutorial (1s) 11

All the required files will be placed in the dist/myreactapp folder and can be deployed to your favorite hosting provider.

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:

Modularizing your React App with Local Libraries

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

1└─ myreactapp 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

Creating Local Libraries

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 each of these areas using the React library generator:

1nx g @nx/react:library products --unitTestRunner=vitest --bundler=none --directory=modules/products 2nx g @nx/react:library orders --unitTestRunner=vitest --bundler=none --directory=modules/orders 3nx g @nx/react:library ui --unitTestRunner=vitest --bundler=none --directory=modules/shared/ui 4
Nx 15 and lower use @nrwl/ instead of @nx/

Note how we use the --directory flag to place the libraries into a subfolder. You can choose whatever folder structure you like, even keep all of them at the root-level.

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

1└─ myreactapp 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.spec.tsx 36 │ │ │ └─ hello-world.tsx 37 │ │ └─ ... 38 │ ├─ ... 39 │ └─ main.tsx 40 ├─ ... 41

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 "@myreactapp/orders": ["modules/orders/src/index.ts"], 6 "@myreactapp/products": ["modules/products/src/index.ts"], 7 "@myreactapp/ui": ["modules/shared/ui/src/index.ts"] 8 }, 9 ... 10 }, 11} 12

Hence we can easily import them into other libraries and our React application. As an example, let's create and expose a ProductList component from our modules/products library. Either create it by hand or run

nx g @nx/react:component product-list --directory=modules/products/src/lib/product-list

Nx 15 and lower use @nrwl/ instead of @nx/

We don't need to implement anything fancy as we just want to learn how to import it into our main React application.

modules/products/src/lib/product-list/product-list.tsx
1import styles from './product-list.module.css'; 2 3/* eslint-disable-next-line */ 4export interface ProductListProps {} 5 6export function ProductList(props: ProductListProps) { 7 return ( 8 <div className={styles['container']}> 9 <h1>Welcome to ProductList!</h1> 10 </div> 11 ); 12} 13 14export default ProductList; 15

Make sure the ProductList is exported via the index.ts file of our products library. This is our public API with the rest of the workspace. Only export what's really necessary to be usable outside the library itself.

modules/products/src/index.ts
1export * from './lib/product-list/product-list'; 2

We're ready to import it into our main application now. 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/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 ProductList component into our app.tsx and render it via the routing mechanism whenever a user hits the /products route.

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

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

products route

Let's apply the same for our orders library.

  • generate a new component OrderList in modules/orders and export it in the corresponding index.ts file
  • import it into the app.tsx 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/app.tsx
1import { Route, Routes } from 'react-router-dom'; 2import { ProductList } from '@myreactapp/products'; 3import { OrderList } from '@myreactapp/orders'; 4 5function Home() { 6 return <h1>Home</h1>; 7} 8 9export function App() { 10 return ( 11 <Routes> 12 <Route path="/" element={<Home />}></Route> 13 <Route path="/products" element={<ProductList />}></Route> 14 <Route path="/orders" element={<OrderList />}></Route> 15 </Routes> 16 ); 17} 18 19export default App; 20

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. Note: you need to restart the nx graph command to update the graph visualization or run the CLI command with the --watch flag.

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. Open the .eslintrc.base.json at the root of the workspace and add the following depConstraints in the @nx/enforce-module-boundaries rule configuration:

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

To test it, go to your modules/products/src/lib/product-list/product-list.tsx file and import the OrderList from the orders project:

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

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

~/workspace

nx run-many -t lint

1 ✔ nx run myreactapp:lint [existing outputs match the cache, left as is] 2 ✔ nx run e2e:lint [existing outputs match the cache, left as is] 3 ✔ nx run ui:lint (1s) 4 5 ✖ nx run products:lint 6 Linting "products"... 7 8 /Users/.../myreactapp/modules/products/src/lib/product-list/product-list.tsx 9 3:1 error A project tagged with "scope:products" can only depend on libs tagged with "scope:products", "scope:shared" @nx/enforce-module-boundaries 10 111 problem (1 error, 0 warnings) 12 13 Lint errors found in the listed files. 14 15 ✔ nx run orders:lint (1s) 16 17 ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 18 19 > NX Ran target lint for 5 projects (1s) 20 214/5 succeeded [2 read from cache] 22 231/5 targets failed, including the following: 24 - nx run products:lint 25
Nx 15 and lower use @nrwl/ instead of @nx/

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

ESLint module boundary 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 myreactapp 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

Next Steps

Here's some more things you can dive into next:

Also, make sure you