This tutorial walks you through creating a React monorepo with Nx. You'll build a small example application to understand the core concepts and workflows.
What you'll learn:
Prerequisites
This tutorial requires a GitHub account to demonstrate the full value of Nx - including task running, caching, and CI integration.
Step 1: Creating a new Nx React workspaceLet's create your workspace. The setup process takes about 2 minutes and will configure React, testing, and CI/CD automatically.
Step 2: Verify Your SetupPlease verify closely that you have the following setup:
.github/workflows/ci.yml
pipeline preconfiguredYou should see your workspace in your Nx Cloud organization.
If you do not see your workspace in Nx Cloud then please follow the steps outlined in the Nx Cloud setup.
This is important for using remote caching and self-healing in CI later in the tutorial.
Let's take a look at the structure of our new Nx workspace:
acme ├── .github │ └── workflows │ └── ci.yml ├── apps │ └── demo ├── README.md ├── eslint.config.mjs ├── nx.json ├── package-lock.json ├── package.json ├── tsconfig.base.json ├── tsconfig.json └── vitest.workspace.ts
Here are some files that might jump to your eyes:
.nx
folder is where Nx stores local metadata about your workspaces using the Nx Daemon.nx.json
file contains configuration settings for Nx itself and global default settings that individual projects inherit..github/workflows/ci.yml
file preconfigures your CI in GitHub Actions to run build and test through Nx.Now, let's build some features and see how Nx helps get us to production faster.
To serve your new React app, run:
The app is served at http://localhost:4200.
Nx uses the following syntax to run tasks:
Inferred TasksBy default Nx simply runs your package.json
scripts. However, you can also adopt Nx technology plugins that help abstract away some of the lower-level config and have Nx manage that. One such thing is to automatically identify tasks that can be run for your project from tooling configuration files such as package.json
scripts and vite.config.ts
.
In nx.json
there's already the @nx/vite
plugin registered which automatically identifies build
, test
, serve
, and other Vite-related targets.
nx.json
{ ... "plugins": [ { "plugin": "@nx/vite/plugin", "options": { "buildTargetName": "build", "testTargetName": "test", "serveTargetName": "serve", "devTargetName": "dev", "previewTargetName": "preview", "serveStaticTargetName": "serve-static", "typecheckTargetName": "typecheck", "buildDepsTargetName": "build-deps", "watchDepsTargetName": "watch-deps" } } ] }
To view the tasks that Nx has detected, look in the Nx Console project detail view or run:
❯
npx nx show project demo
Project Details View (Simplified)
Root: apps/demo
Type:application
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
. 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:
apps/demo/vite.config.ts
export default defineConfig({ build: { outDir: './build', }, });
Now if you look at the project details view, the outputs for the build target will say {projectRoot}/build
. The @nx/vite
plugin ensures that tasks and their options, such as outputs, are automatically and correctly configured.
Overriding inferred task options
You can override the options for inferred tasks by modifying the targetDefaults
in nx.json
or setting a value in your package.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.
When you develop your React application, usually all your logic sits in the app's src
folder. Ideally separated by various folder names which represent your domains or features. As your app grows, however, the app becomes more and more monolithic, which makes building and testing it harder and slower.
acme ├── apps │ └── demo │ └── src │ ├── app │ ├── cart │ ├── products │ ├── orders │ └── ui └── ...
Nx allows you to separate this logic into "local libraries." The main benefits include
Let's create a reusable design system library called ui
that we can use across our workspace. This library will contain reusable components such as buttons, inputs, and other UI elements.
npx nx g @nx/react:library packages/ui --unitTestRunner=vitest --bundler=none
Note how we type out the full path in the directory
flag to place the library into a subfolder. You can choose whatever folder structure you like to organize your projects.
Running the above commands should lead to the following directory structure:
acme ├── apps │ └── demo ├── packages │ └── ui ├── eslint.config.mjs ├── nx.json ├── package-lock.json ├── package.json ├── tsconfig.base.json ├── tsconfig.json └── vitest.workspace.ts
Just as with the demo
app, Nx automatically infers the tasks for the ui
library from its configuration files. You can view them by running:
In this case, we have the lint
and test
tasks available, among other inferred tasks.
All libraries that we generate are automatically included in the workspaces
defined in the root-level package.json
.
package.json
{ "workspaces": ["apps/*", "packages/*"] }
Hence, we can easily import them into other libraries and our React application.
You can see that the AcmeUi
component is exported via the index.ts
file of our ui
library so that other projects in the repository can use it. This is our public API with the rest of the workspace and is enforced by the exports
field in the package.json
file. Only export what's necessary to be usable outside the library itself.
packages/ui/src/index.ts
export * from './lib/ui';
Let's add a simple Hero
component that we can use in our demo app.
packages/ui/src/lib/hero.tsx
export function Hero(props: { title: string; subtitle: string; cta: string; onCtaClick?: () => void; }) { return ( <div style={{ backgroundColor: '#1a1a2e', color: 'white', padding: '100px 20px', textAlign: 'center', }} > <h1 style={{ fontSize: '48px', marginBottom: '16px', }} > {props.title} </h1> <p style={{ fontSize: '20px', marginBottom: '32px', }} > {props.subtitle} </p> <button onClick={props.onCtaClick} style={{ backgroundColor: '#0066ff', color: 'white', border: 'none', padding: '12px 24px', fontSize: '18px', borderRadius: '4px', cursor: 'pointer', }} > {props.cta} </button> </div> ); }
Then, export it from index.ts
.
packages/ui/src/index.ts
export * from './lib/hero'; export * from './lib/ui';
We're ready to import it into our main application now.
apps/demo/src/app/app.tsx
import { Route, Routes } from 'react-router-dom'; import { Hero } from '@acme/ui'; export function App() { return ( <> <h1>Home</h1> <Hero title="Welcmoe to our Demo" subtitle="Build something amazing today" cta="Get Started" /> </> ); } export default App;
Serve your app again (npx nx serve demo
) and you should see the new Hero component from the ui
library rendered on the home page.
If you have keen eyes, you may have noticed that there is a typo in the App
component. This mistake is intentional, and we'll see later how Nx can fix this issue automatically in CI.
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 npx nx build
, enabling intelligent caching, and more. Interestingly, you can also visualize it.
Just run:
You should be able to see something similar to the following in your browser.
Let's create a git branch with the new hero component so we can open a pull request later:
❯
git checkout -b add-hero-component
❯
git commit -m 'add hero component'
Our current setup doesn't just come with targets for serving and building the React application, but also has targets for testing and linting. We can use the same syntax as before to run these tasks:
❯
npx nx test demo # runs the tests for demo
❯
npx nx lint ui # runs the linter on ui
More conveniently, we can also run tasks in parallel using the following syntax:
❯
npx nx run-many -t test lint
This is exactly what is configured in .github/workflows/ci.yml
for the CI pipeline. The run-many
command allows you to run multiple tasks across multiple projects in parallel, which is particularly useful in a monorepo setup.
There is a test failure for the demo
app due to the updated content. Don't worry about it for now, we'll fix it in a moment with the help of Nx Cloud's self-healing feature.
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.
~/acme❯
npx nx run-many -t test lint
✔ nx run @acme/ui:lint ✔ nx run @acme/ui:test ✔ nx run @acme/demo:lint ✖ nx run @acme/demo:test ————————————————————————————————————————————————————————————————————————————————————————————————————————— NX Ran targets test, lint for 2 projects (1s) ✔ 3/4 succeeded [3 read from cache] ✖ 1/4 targets failed, including the following: - nx run @acme/demo:test
Again, the @acme/demo:test
task failed, but notice that the remaining three tasks were read from cache.
Not all tasks might be cacheable though. You can configure the cache
settings in the targetDefaults
property of the nx.json
file. You can also learn more about how caching works.
In this section, we'll explore how Nx Cloud can help your pull request get to green faster with self-healing CI. Recall that our demo app has a test failure, so let's see how this can be automatically resolved.
The npx nx-cloud fix-ci
command that is already included in your GitHub Actions workflow (github/workflows/ci.yml
) is responsible for enabling self-healing CI and will automatically suggest fixes to your failing tasks.
.github/workflows/ci.yml
name: CI on: push: branches: - main pull_request: permissions: actions: read contents: read jobs: main: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: filter: tree:0 fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci --legacy-peer-deps - run: npx nx run-many -t lint test build - run: npx nx-cloud fix-ci if: always()
You will also need to install the Nx Console editor extension for VS Code, Cursor, or IntelliJ. For the complete AI setup guide, see our AI integration documentation.
Now, let's push the add-hero-component
branch to GitHub and open a new pull request.
❯
git push origin add-hero-component
❯
# Don't forget to open a pull request on GitHub
As expected, the CI check fails because of the test failure in the demo
app. But rather than looking at the pull request, Nx Console notifies you that the run has completed, and that it has a suggested fix for the failing test. This means that you don't have to waste time babysitting your PRs, and the fix can be applied directly from your editor.
From the Nx Console notification, you can click Show Suggested Fix
button. Review the suggested fix, which in this case is to change the typo Welcmoe
to the correct Welcome
spelling. Approve this fix by clicking ApplyFix
and that's it!
You didn't have to leave your editor or do any manual work to fix it. This is the power of self-healing CI with Nx Cloud.
Remote Cache for Faster Time To GreenAfter the fix has been applied and committed, CI will re-run automatically, and you will be notified of the results in your editor.
When you click View Results
to show the run in Nx Cloud, you'll notice something interesting. The lint and test tasks for the ui
library were read from remote cache and did not have to run again, thus each taking less than a second to complete.
This happens because Nx Cloud caches the results of tasks and reuses them across different CI runs. As long as the inputs for each task have not changed (e.g. source code), then their results can be replayed from Nx Cloud's Remote Cache. In this case, since the last fix was applied only to the demo
app's source code, none of the tasks for ui
library had to be run again.
This significantly speeds up the time to green for your pull requests, because subsequent changes to them have a good chance to replay tasks from cache.
Remote Cache Outputs
Outputs from cached tasks, such as the dist
folder for builds or coverage
folder for tests, are also read from cache. Even though a task was not run again, its outputs are available. The Cache Task Results page provides more details on caching.
This pull request is now ready to be merged with the help of Nx Cloud's self-healing CI and remote caching.
Here are some things you can dive into next:
Also, make sure you
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4