Effective React With NX 2022
Effective React With NX 2022
Effective React With NX 2022
This is a Leanpub book. Leanpub empowers authors and publishers with the
Lean Publishing process. Lean Publishing is the act of publishing an
in-progress ebook using lightweight tools and many iterations to get reader
feedback, pivot until you have the right book and build traction once you do.
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Monorepos to the rescue! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Why Nx? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Is this book for you? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
How this book is laid out . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Chapter 2: Libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Apps and Libs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
The generate command . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
Feature libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
UI libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Using the UI library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Data-access libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
Enforcing module boundaries . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
• Late discovery of bugs because they can only occur at the point of integra-
tion rather than when code is changed.
monorepo approach - when done correctly - can save developers from a great
deal of headache and wasted time.
• Monorepos are monolithic and not for building microservices and mi-
crofrontends1
Why Nx?
Nx is a fast, smart and extensible build system that helps teams develop
applications at any scale. It integrates with modern frameworks and libraries,
provides computation caching and smart rebuilds, as well as code generators.
This book assumes that you have prior experience working with React, so it does
not go over any of the basics. We will also make light use of the Hooks API,
however understanding it is not necessary to grasp the concepts in this book.
• You just heard about Nx and want to know more about how it applies to
React development.
• You use React at work and want to learn tools and concepts to help your
team work more effectively.
• You want to use great tools that enable you to focus on product development
rather than environment setup.
• You use a monorepo but have struggled with its setup. Or perhaps you want
to use a monorepo but are unsure how to set it up.
• You are pragmatic person who learns best by following practical examples
of an application development.
On the other hand, this book might not be for you if:
• You are already proficient at using Nx with React and this book may not
teach you anything new.
• You hate monorepos so much that you cannot stand looking at them.
Okay, the last bullet point is a bit of a joke, but there are common concerns
regarding monorepos in practice.
Workspace
A folder created using Nx that contains applications and libraries, as well
as scaffolding to help with building, linting, and testing.
Project
An application or library within the workspace.
Application
A package that uses multiple libraries to form a runnable program. An
application is usually either run in the browser or by Node.
Library
A set of files that deal with related concerns. For example, a shared compo-
nent library.
Creating a Nx workspace
npx create-nx-workspace@latest
Note: The npx binary comes bundled with NodeJS. It allows you to con-
veniently install then run a Node binary without the need to install it
globally.
Chapter 1: Getting started 6
Nx will ask you for a workspace name. Let’s use acme as it is the name of
our imaginary organization. The workspace name is used by Nx to scope our
libraries, just like npm scoped packages.
Creating a workspace
After choosing the preset, you’ll be prompted for the application name, and the
styling format you want to use. Let’s use bookstore as our application name and
styled-components for styling.
Chapter 1: Getting started 7
Note: If you
prefer Yarn over npm, you can pass the
--packageManager=yarn flag to the create-nx-workspace.
Once Nx finishes creating the workspace, we will end up with something like
this:
2 https://nx.app
Chapter 1: Getting started 8
.
├── apps
│ ├── bookstore
│ │ ├── src
│ │ ├── jest.config.js
│ │ ├── project.json
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ └── tsconfig.spec.json
│ └── bookstore-e2e
│ ├── src
│ ├── cypress.json
│ ├── project.json
│ └── tsconfig.json
├── libs
├── babel.config.json
├── jest.config.js
├── jest.preset.js
├── README.md
├── nx.json
├── package-lock.json
├── package.json
├── tools
│ ├── generators
│ └── tsconfig.tools.json
├── tsconfig.base.json
└── workspace.json
The apps folder contains the code of all applications in our workspace. Nx has
created two applications by default:
The libs folder will eventually contain our libraries (more on that in Chapter 2).
It is empty for now.
3 https://www.cypress.io/
Chapter 1: Getting started 9
The tools folder can be used for scripts that are specific to the workspace. The
generated tools/generators folder is for Nx’s workspace generators feature
which you can learn more about by reading the documentation at https://nx.dev/generators/w
generators.
The nx.json file configures Nx. We’re going to have a closer look at that in
Chapter 4.
npm start
The above command uses the start script in the main package.json which
builds the bookstore application and then starts a development server at port
4200.
Nx workspace configuration
This allows to gradually dive deeper into the Nx features or simply to just start
in a more lightweight fasion. You could easily just use the Nx core5 and rely on
other tooling such as Yarn/Npm workspaces to do the linking. Yet, you would
miss out on a lot of features.
In this book we’re going full-in. This allows us to explore all the features Nx can
bring to the table when it comes to React development and thus set us up to be
most productive. This setup comes with some configuration files that provide
Nx with the necessary meta-data to be able to best reason about the structure
of the underlying workspace. Let’s briefly explore them in more detail.
The previously generated workspace comes with the follow Nx specific configu-
ration files:
• nx.json
• workspace.json
• project.json
The nx.json is at the root of the workspace and configures the Nx CLI. It allows to
specify things such as defaults for projects and code scaffolding, the workspace
layout, task runner options and computation cache configuration and more.
Here’s an excerpt of what got generated for our example workspace.
{
"npmScope": "acme",
"affected": {
"defaultBase": "main"
},
"cli": {
"defaultCollection": "@nrwl/react"
},
"implicitDependencies": {
"package.json": {
"dependencies": "*",
"devDependencies": "*"
5 https://nx.dev/getting-started/nx-core
Chapter 1: Getting started 12
},
".eslintrc.json": "*"
},
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/workspace/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"]
}
}
},
"targetDependencies": {
"build": [
{
"target": "build",
"projects": "dependencies"
}
]
},
"generators": {
"@nrwl/react": {
"application": {
"style": "styled-components",
"linter": "eslint",
"babel": true
},
...
}
},
...
}
The workspace.json file in the root directory is optional. It’s used to list the
projects in your workspace explicitly, instead of having Nx scan the file tree for
all project.json and package.json files.
The project.json file is located at the root of every project in your workspace.
This is where the project specific metadata is defined as well as the “targets”.
A Nx target is literally a “task” that can be invoked on the project. Open the
Chapter 1: Getting started 13
{
"root": "apps/bookstore",
"sourceRoot": "apps/bookstore/src",
"projectType": "application",
"targets": {
"build": { ... },
"serve": { ... },
"lint": { ... }
"test": { ... }
},
...
}
It contains targets for invoking a build, serve for serving the app during
development as well as targets for linting (lint) and testing (test). These are
the ones generated by default, but you are free to add your own as well.
Each of these targets comes with a set of things that can be configured. Let’s
have a look at the build target:
{
...
"targets": {
"build": {
"executor": "@nrwl/web:webpack",
"outputs": ["{options.outputPath}"],
"options": {
"compiler": "babel",
"outputPath": "dist/apps/bookstore",
"index": "apps/bookstore/src/index.html",
"baseHref": "/",
"main": "apps/bookstore/src/main.tsx",
"polyfills": "apps/bookstore/src/polyfills.ts",
"tsConfig": "apps/bookstore/tsconfig.app.json",
"assets": [
"apps/bookstore/src/favicon.ico",
Chapter 1: Getting started 14
"apps/bookstore/src/assets"
],
"styles": [],
"scripts": [],
"webpackConfig": "@nrwl/react/plugins/webpack"
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "apps/bookstore/src/environments/environment.ts",
"with": "apps/bookstore/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false
}
}
},
...
},
...
}
options and you can totally also create your own custom executor7 .
The target comes also with options that are read by the executor to customize
the outcome accordingly. Depending on the executor implementation the tar-
get is using, these options might vary.
Finally there’s the configurations which extends the options and potentially
overrides them with different values. This can be handy when building for dif-
ferent environments. Configurations can be activate by passing the --configuration=<name>
flag to the command.
Nx commands
As we mentioned in the previous section, targets can be invoked. You can curn
them in the form: nx [target] [project].
For example, for our bookstore app we can run the following targets.
There isn’t much in the workspace to make this graph useful just yet, but we will
see in later chapters how this feature can help us understand the architecture
of our application, and how changes to code affect various projects within the
workspace.
It’s easier to work with Nx when we have it installed globally. You can do this
by running:
Chapter 1: Getting started 18
Now you will be able to run Nx commands without going through npx (e.g. nx
serve bookstore).
For the rest of this book, I will assume that you have Nx installed globally. If you
haven’t, simply run all issued commands through npx.
Let’s end this chapter by removing the generated content from the bookstore
application and adding some configuration to the workspace.
apps/bookstore/src/app/app.tsx
apps/bookstore/src/app/app.spec.tsx
Chapter 1: Getting started 19
describe('App', () => {
afterEach(cleanup);
expect(baseElement).toBeTruthy();
});
expect(getByText('Bookstore')).toBeTruthy();
});
});
apps/bookstore-e2e/src/integration/app.spec.ts
describe('bookstore', () => {
beforeEach(() => cy.visit('/'));
• nx lint bookstore
• nx test bookstore
• nx e2e bookstore-e2e
Chapter 1: Getting started 20
It’s a good idea to commit our code before making any more changes.
git add .
git commit -m 'end of chapter one'
Key points
A newly created workspace comes with a set of targets we can run on the
generated application: lint, test, and e2e.
Nx also has a tool for displaying the dependency graph of all the projects
within the workspace.
Chapter 2: Libraries
We have the skeleton of our application from Chapter 1.
So now we can start adding to our application by creating and using libraries.
Before we dive straight into creating libraries, though, let’s first understand
the concept of libraries in an Nx workspace.
Note, these libraries don’t necessarily need to be built separately, but are
rather consumed and built by the application itself directly. Hence, nothing
changes from a pure deployment point of view. That said, it is totally possible
to create so-called “buildable libraries” for enabling incremental builds8 as
well as “publishable libraries” for those scenarios where not only you want to
use a specific library within the current Nx workspace, but also to publish it
to some package repository (e.g NPM). You can read more about buildable and
publishable libraries on the official Nx docs9 .
Organizing Libraries
Developers new to Nx are initially often hesitant to move their logic into
libraries, because they assume it implies that those libraries need to be general
purpose and shareable across applications. This is a common misconception.
Moving code into libraries can be done from a pure code organization perspec-
tive.
In fact when organizing libraries you should think about your business domains.
Most often teams are aligned with those domains and thus a similar organiza-
tion of the libraries in the libs folder might be most appropriate. Nx allows to
nest libraries into sub-folders which makes it easy to reflect such structuring.
8 https://nx.dev/ci/incremental-builds
9 https://nx.dev/structure/buildable-and-publishable-libraries
Chapter 2: Libraries 23
.
├── (...)
├── libs
│ └── books
│ │ └── feature
│ │ │ ├── src
│ │ │ ├── ...
│ │ │ └── ...
│ │ └── ui
│ │ ├── src
│ │ ├── ...
│ │ └── ...
│ └── ui
│ ├── src
│ ├── ...
│ └── ...
└── (...)
Categories of libraries
Feature
Libraries that implement “smart” UI (e.g. is effectful, is connected to data
sources, handles routing, etc.) for specific business use cases.
Data-access
Libraries that contain the means for interacting with external data services;
external services are typically backend services.
Utility
Libraries that contain common utilities that are shared by many projects.
More concretely, we can form rules about what each types of libraries can
depend on. For example, UI libraries cannot use feature or data-access libraries,
because doing so will mean that they are effectful.
We’ll see in later in this chapter how we can use Nx to strictly enforce these
boundaries.
Feature libraries
nx g lib feature \
--directory books \
--appProject bookstore \
--tags type:feature,scope:books
Chapter 2: Libraries 25
The --directory option allows us to group our libraries by nesting them under
their parent directory. In this case the library is created in the libs/books/feature
folder. It is aliased to -d.
The --appProject option lets Nx know that we want to make our feature library
to be routable inside the specified application. This option is useful because Nx
will do three things for us.
The --tags option lets us annotate our applications and libraries to express
constraints within the workspace. The tags are added to project.json, and
we’ll see at the end of this chapter how they can be used to enforce different
constraints.
Pro-tip: You can pass the --dryRun option to generate to see the effects
of the command before committing to disk.
Once the command completes, you should see the new directory.
.
├── (...)
├── libs
│ └── books
│ └── feature
│ ├── src
│ │ ├── index.ts
│ │ └── lib
│ ├── jest.config.js
│ ├── project.json
│ ├── README.md
Chapter 2: Libraries 26
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ └── tsconfig.spec.json
└── (...)
Nx generated our library with some default code as well as scaffolding for linting
(ESLint) and testing (Jest). You can run them with:
nx lint books-feature
nx test books-feature
You’ll also see that the App component for bookstore has been updated to
include the new route.
<li>
<Link to="/feature">BooksFeature</Link>
</li>
<li>
<Link to="/page-2">Page 2</Link>
</li>
</ul>
</div>
<Route
path="/"
exact
render={() => (
<div>
This is the generated root route.{' '}
<Link to="/page-2">Click here for page 2.</Link>
</div>
)}
/>
<Route path="/feature" component={BooksFeature} />
<Route
path="/page-2"
exact
render={() => (
<div>
<Link to="/">Click here to go back to root page.</Link>
</div>
)}
/>
{/* END: routes */}
</StyledApp>
);
};
export default App;
Additionally, the main.tsx file for bookstore has also been updated to render
<BrowserRouter />. This render is needed in order for <Route /> components to
work, and Nx will handle the file update for us if necessary.
Chapter 2: Libraries 28
ReactDOM.render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
document.getElementById('root')
);
Restart the development server again (nx serve bookstore), and you should see
the updated application.
Be aware when you add a new project to the workspace, you must
restart your development server. This restart is necessary in order
for the TypeScript compiler to pick up new library paths, such as
@acme/books/feature.
By using a monorepo, we’ve skipped a few steps that are usually required when
creating a new library.
• Setting up the CI
And now we have our library! Wasn’t that easy? Something that may have taken
minutes or hours–sometimes even days–now takes only takes a few seconds.
Chapter 2: Libraries 29
Let’s remedy this situation by adding a component library that will provide
better styling.
UI libraries
nx g lib ui \
--tags type:ui,scope:books \
--no-interactive
The --no-interactive tells Nx to not prompt us with options, but instead use
the default values.
Please note that we will make heavy use of styled-components in this com-
ponent library. Don’t fret if you’re not familiar with styled-components.
If you know CSS then you should not have a problem understanding this
section. To learn more about styled-components you can check our their
documentation.
acme
├── (...)
├── libs
│ ├── (...)
│ ├── ui
│ │ ├── src
│ │ │ ├── lib
│ │ │ └── index.ts
│ │ ├── .eslintrc
│ │ ├── jest.config.js
│ │ ├── README.md
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ └── tsconfig.spec.json
└── (...)
This library isn’t quite useful yet, so let’s add in some components.
Chapter 2: Libraries 31
The --project option specifies which project (as found in the projects section
of workspace.json) to add the new component to. It is aliased to -p.
The --export option tells Nx to export the new component in the index.ts file
of the project so that it can be imported elsewhere in the workspace. You may
leave this option off if you are generating private/internal components. It is
aliased to -e.
If you do forget the --export option you can always manually add the export
barrel to index.ts.
Next, let’s go over the implementation of each of the components and what
their purposes are.
GlobalStyles
This component injects a global stylesheet into our application when used. It is
particularly useful for overriding global style rules such as body { margin: 0 }.
libs/ui/src/lib/global-styles/global-styles.tsx
Chapter 2: Libraries 32
* {
box-sizing: border-box;
}
`;
Button
libs/ui/src/lib/button/button.tsx
&:hover {
background-color: #80a8e2;
border-color: #0e2147;
}
Chapter 2: Libraries 33
`;
These two components are used for layout. The header component forms the
top header bar, while the main component takes up the rest of the page.
libs/ui/src/lib/header/header.tsx
a {
color: white;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
Chapter 2: Libraries 34
> h1 {
margin: 0 1rem 0 0;
padding-right: 1rem;
border-right: 1px solid white;
}
`;
libs/ui/src/lib/main/main.tsx
And finally, the NavigationList and NavigationItem components will render the
navigation bar inside our top Header component.
libs/ui/src/lib/navigation-list/navigation-list.tsx
Chapter 2: Libraries 35
libs/ui/src/lib/navigation-item/navigation-item.tsx
Now we can use the new library in our bookstore’s app component.
apps/bookstore/src/app/app.tsx
Finally, let’s restart our server (nx serve bookstore) and we will see a much
improved UI.
git add .
git commit -m 'add books feature and ui components'
That’s great, but we are still not seeing any books, so let’s do something about
this.
Chapter 2: Libraries 38
Data-access libraries
What we want to do is fetch data from somewhere and display that in our books
feature. Since we will be calling a backend service we should create a new data-
access library.
You may have noticed that we are using a prefix @nrwl/web:lib instead of
just lib like in our previous examples. This @nrwl/web:lib syntax means that
we want Nx to run the lib (or library) generator provided by the @nrwl/web
collection.
We were able to go without this prefix previously because the nx.json configu-
ration has set @nrwl/react as the default option.
{
// ...
"cli": {
"defaultCollection": "@nrwl/react"
},
// ...
}
Back to the example. Let’s modify the library to export a getBooks function to
load our list of books.
libs/books/data-access/src/lib/books-data-access.ts
The next step is to use the getBooks function within our books feature. We can
do this with React’s useEffect and useState hooks.
libs/books/feature/src/lib/books-feature.tsx
useEffect(() => {
getBooks().then(setBooks);
}, [
// This effect runs only once on first component render
// so we declare it as having no dependent state.
]);
return (
<>
<h2>Books</h2>
<Books books={books} />
</>
);
};
Chapter 2: Libraries 41
You’ll notice that we’re using two new components: Books and Book. They can
be created as follows.
Again, we will see later in this chapter how Nx enforces module boundaries.
libs/books/ui/src/lib/books/books.tsx
return (
<StyledBooks>
{books.map(book => (
<Book key={book.id} book={book} />
))}
</StyledBooks>
);
};
libs/books/ui/src/lib/book/book.tsx
That’s great and all, but you may have observed a couple of problems.
1. The getBooks data-access function is a stub and doesn’t actually call out to
a backend service.
2. We’ve been using any types when dealing with books data. For example, the
return type of getBooks is any[] and our BookProp takes specifies { book: any
}. This makes our code unsafe and can lead to production bugs.
We’ll address both problems in the next chapter. For now, let’s wrap up by
examining how Nx can enforce module boundaries between different types of
libraries that we’ve created in this chapter.
Chapter 2: Libraries 45
Recall that earlier, when we generated our libraries we passed the --tags option
to define a type and scope for each of them. Let’s examine how we can use these
tags to define and enforce clean separation of concerns within the workspace.
Open up the .eslintrc.json file at the root of your workspace. It will contain an
entry for @nrwl/nx/enforce-module-boundaries as follows.
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
],
"allow": [],
"enforceBuildableLibDependency": true
}
]
The depConstraints section is the one you will be spending most time fine-tun-
ing. It is an array of constraints, each consisting of sourceTag and onlyDependOnLibsWithTags
properties. The default configuration has a wildcard * set as a value for both of
them, meaning that any project can import (depend on) any other project.
The circular dependency chains such as lib A -> lib B -> lib C -> lib A are
also not allowed. The self circular dependency (when lib imports from a named
alias of itself), while not recommended, can be overridden by setting the flag
allowCircularSelfDependency to true.
Chapter 2: Libraries 46
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
"allowCircularSelfDependency": true,
...
}
]
The allow array is a whitelist listing the import definitions that should be
omitted from further checks. We will see how overrides work after we define
the depConstraints section.
1. Feature
2. UI
3. Data-access
4. Utility
We’ve already added these types as tags to our libraries when we ran the
generate command (e.g. --tags type:ui). We also want to consider a fifth type
of project in the workspace: type:app that we can tag all of our applications with.
Now, let’s define some constraints for what each types of projects can depend
on.
• Applications can depend on any types of libraries, but not other applica-
tions.
Chapter 2: Libraries 47
Let’s see how we can configure the ESLint rule to enforce the above constraints.
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
...
"depConstraints": [
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:ui", "type:util"]
},
{
"sourceTag": "type:util",
"onlyDependOnLibsWithTags": ["type:util"]
}
]
}
]
Further, recall that we also have a second dimension to the tags of our libraries
(e.g. --tag scope:books). The scope tag allows us to separate our applications
and libraries into logical domains.
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
...
"depConstraints": [
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
},
...
{
"sourceTag": "scope:books",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:books"]
},
{
"sourceTag": "scope:admin",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:admin"]
},
{
"sourceTag": "scope:shared",
Chapter 2: Libraries 49
"onlyDependOnLibsWithTags": ["scope:shared"]
}
]
}
]
These module boundaries are needed as the workspace grows, otherwise projects
will become unmanageable, and changes will be hard to reason about. You can
customize the tags however you want. If you have a multi-platform monorepo,
you might add platform:web, platform:node, platform:native, and platform:all
tags.
Learn more about configuring boundaries with the Taming Code Organization
with Module Boundaries in Nx article10 .
Finally, let’s commit our changes up to this point before moving on to the next
chapter.
git add .
git commit -m 'implement books feature and link to application'
10 https://blog.nrwl.io/mastering-the-project-boundaries-in-nx-f095852f5bf4
Chapter 2: Libraries 50
Key points
There are four type of libraries: feature, UI, data-access, and util.
nx dep-graph
Chapter 3: Working effectively in a monorepo 52
Note that you can also manually add so-called “implicit dependencies”
for those rare cases where there needs to be a dependency which can not
be automatically inferred from source code. Read more about that here:
https://nx.dev/configuration/projectjson#implicitdependencies
Chapter 3: Working effectively in a monorepo 53
Let’s say we want to add a checkout button to each of the books in the list.
We can update our Book, Books, and BooksFeature components to pass along a
new onAdd callback prop.
libs/books/ui/src/lib/book/book.tsx
`;
libs/books/ui/src/lib/books/books.tsx
<StyledBooks>
{books.map(book => (
// Pass down new callback prop
<Book key={book.id} book={book} onAdd={onAdd} />
))}
</StyledBooks>
);
};
libs/books/feature/src/lib/books-feature.tsx
useEffect(() => {
getBooks().then(setBooks);
}, [
// This effect runs only once on first component render
// so we declare it as having no dependent state.
]);
return (
<>
<h2>Books</h2>
{/* Pass a stub callback for now */}
{/* We'll implement this properly in Chapter 4 */}
<Books books={books} onAdd={book => alert(`Added ${book.title}`)} />
</>
);
};
By leveraging the dependency graph, Nx is not only able to understand how the
workspace projects relate to each other, but combining this with the Git history,
Nx is able to determine which projects were affected by a given changeset.
We can ask Nx to show us how this change affects the projects within our
workspace using the so-called “affected command”.
nx affected:dep-graph
Affected dependencies
As we can see, Nx knows that the books-ui library has changed starting from
the Git main branch. Using this information, Nx walks up the dependency graph
and highlights all the dependent projects affected by this change in red.
Chapter 3: Working effectively in a monorepo 57
But there is more. We can not only just visualize this change, but we can use
various commands to run only against this affected set of projects. Hence, we
can just re-test, re-lint or re-build what changed.
Nx topologically sorts the projects such that they are run from bottom to top.
That is, projects at the bottom of the dependency chain run first. All these tasks
are also parallelized by default (you can customize the amount of parallel tasks
using --maxParallel).
Nx uses 3 parallel tasks by default. You can customize the amount using
the --maxParallel flag.
All of the affected:* commands use the Git history, comparing the current
HEAD with a “base” to determine which Nx project(s) got changed. By default
“base” refers to the main branch. You can customize that by either passing the
--base flag to the command or by changing the defaultBase property in nx.json.
Note that in these projects, Nx is using Jest and Cypress to run unit and e2e
tests respectively. They make writing and running tests are fast and simple as
possible. If you’re not familiar with them, please read their documentation to
learn more.
So far we haven’t been diligent about verifying that our changes are okay, so
unsurprisingly our tests are failing.
I’ll leave it to you as an exercise to fix the broken unit and e2e tests. A hint
for the App component test, you should look into the MemoryRouter from React
Router.
For the full solution please see the bookstore example repository: https://github.com/jaysoo
react-book-example.
Computation Caching
When you heavily adopt a monorepo and the number of projects grows, you need
to start thinking about scaling. We have already seen how Nx can cut down the
amount of projects to recompute drastically by using the previously mentioned
“affected” commands.
Chapter 3: Working effectively in a monorepo 59
Let’s take the example of running a unit test for an application app1. By default
the computation hash includes
While this is the default behavior, it can also be customized to more specific
needs. For instance, lint checks may only depend on the source code of the
project and global configs. Or similarly, builds may depend on the dts files of
the compiled libs instead of their source.
Once Nx has the computation hash, it verifies whether that specific hash
already exists in its cache. If it does, it replays the task’s output in the terminal
and restores all possible files in the right folders. From a developers perspective
it looks like the task was just run, simply a lot faster.
Try it out by yourself by running unit tests for the books-feature project. Run it
once and then again to see it being restored from the cache the 2nd time.
{
...
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/workspace/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"]
}
}
},
}
You can get even more benefits if this cache is not only local, but remotely
distributed. Such functionality can be enabled by using Nx Cloud11 .
11 https://nx.app
Chapter 3: Working effectively in a monorepo 61
If Nx Cloud is enabled, the local cache folder will be synced with a cloud-hosted,
remote counterpart. With the remote cache, other team members and CI agents
can read from it too and drastically reduce the required computation time. Learn
more on https://nx.app and the corresponding Nx Cloud docs at https://nx.app/
docs.
It’s time to get more practical again and commit your changes if you haven’t
done so already: git add . ; git commit -m 'added checkout button'.
So far our bookstore application does not communicate with a real backend
service. Let’s create one using the Express framework.
nx g @nrwl/express:app api \
--no-interactive \
--frontend-project=bookstore \
--dryRun
nx g @nrwl/express:app api \
--no-interactive \
--frontend-project=bookstore
Just like our frontend application, we can use Nx to serve the API.
nx serve api
Next, let’s implement the /api/books endpoint so that we can use it in our
books-data-access library.
apps/api/src/main.ts
},
{
id: 2,
title: 'Frankenstein',
author: 'Mary Wollstonecraft Shelley',
rating: 4,
price: 7.95
},
{
id: 3,
title: 'Jane Eyre',
author: 'Charlotte Brontë',
rating: 4.5,
price: 10.95
},
{
id: 4,
title: 'Dracula',
author: 'Bram Stoker',
rating: 4,
price: 14.99
},
{
id: 5,
title: 'Pride and Prejudice',
author: 'Jane Austen',
rating: 4.5,
price: 12.85
}
];
res.send(books);
});
libs/books/data-access/src/lib/books-data-access.ts
Let’s commit our changes: git add . ; git commit -am 'added api app'.
Recall that we previously used the any type when working with books data. This
is bad practice as it may lead to uncaught type errors in production.
A better idea would be to create a utility library containing some shared models
to be used by both the frontend and backend.
libs/shared-models/src/lib/shared-models.ts
Chapter 3: Working effectively in a monorepo 67
And now we can update the following five files to use the new model:
apps/api/src/main.ts
// ...
libs/books/data-access/src/lib/books-data-access.ts
libs/books/feature/src/lib/books-feature.tsx
Chapter 3: Working effectively in a monorepo 68
...
import { IBook } from '@acme/shared-models';
// ...
return (
<>
<h2>Books</h2>
<Books books={books} onAdd={book => alert(`Added ${book.title}`)} />
</>
);
};
libs/books/ui/src/lib/books/books.tsx
// ...
import { IBook } from '@acme/shared-models';
// ...
libs/books/ui/src/lib/book/book.tsx
Chapter 3: Working effectively in a monorepo 69
// ...
import { IBook } from '@acme/shared-models';
// ...
By using Nx, we have created a shared model library and refactored both
frontend and backend code in about a minute.
One of the easiest ways to waste time as a developer is on code style. We can
spend hours debating with one another on whether we should use semicolons
or not (you should); or whether we should use a comma-first style or not (you
should not).
Prettier was created to stop these endless debates over code style. It is highly
opinionated and provides minimal configuration options. Best of all, it can
format our code automatically. This means that we no longer need to manually
fix code to conform to the code style.
Nx workspaces come with Prettier installed from the get-go. With it, we can
check the formatting of the workspace, and format workspace code automati-
cally.
Key points
Nx can retest and rebuild only the affected projects within our workspace.
Let’s add the new shared models for our shopping cart.
libs/shared-models/src/lib/shared-models.ts
// ...
apps/api/src/main.ts
Chapter 4: Bringing it all together 73
// ...
This endpoint doesn’t do anything except log and return a fake order number.
In a real world application you would interact with a database or perhaps a
microservice. Since this book is about React development, we will gloss over
the implementation details of this endpoint.
Now we can add our shopping cart data-access library to the frontend applica-
tion.
The cart data-access library should provide a checkout function we can use in
our feature.
libs/cart/data-access/src/lib/cart-data-access.ts
Chapter 4: Bringing it all together 74
The cart state will contain multiple sub-values (cart items, status flag, etc.), and
we’ll also need to communicate with the API endpoint. To help use manage this
complexity, we can take advantage of Redux Toolkit. Luckily, Nx comes with a
generator to help set this up.
Here, we are creating a new Redux slice cart in the cart-data-access library
that we created previously. As well, the generator will install the necessary npm
packages for Redux Toolkit, add configure the store in bookstore app, and add
the cart slice.
// ...
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
</Provider>,
document.getElementById('root')
);
libs/cart/data-access/src/lib/cart.slice.ts
Chapter 4: Bringing it all together 76
import {
createAsyncThunk,
createEntityAdapter,
createSelector,
createSlice,
EntityState,
PayloadAction,
} from '@reduxjs/toolkit';
/*
* Update these interfaces according to your requirements.
*/
export interface CartEntity {
id: number;
}
/**
* Export an effect using createAsyncThunk from
* the Redux Toolkit: https://redux-toolkit.js.org/api/createAsyncThunk
*
* e.g.
* \```
* import { useEffect } from 'react';
* import { useDispatch } from 'react-redux';
*
* // ...
*
* const dispatch = useDispatch();
* useEffect(() => {
* dispatch(fetchCart())
Chapter 4: Bringing it all together 77
* }, [dispatch]);
* \```
*/
export const fetchCart = createAsyncThunk(
'cart/fetchStatus',
async (_, thunkAPI) => {
/**
* Replace this with your custom fetch call.
* For example, `return myApi.getCarts()`;
* Right now we just return an empty array.
*/
return Promise.resolve([]);
}
);
)
.addCase(fetchCart.rejected, (state: CartState, action) => {
state.loadingStatus = 'error';
state.error = action.error.message;
});
},
});
/*
* Export reducer for store configuration.
*/
export const cartReducer = cartSlice.reducer;
/*
* Export action creators to be dispatched. For use with the `useDispatch` ho\
ok.
*
* e.g.
* \```
* import { useEffect } from 'react';
* import { useDispatch } from 'react-redux';
*
* // ...
*
* const dispatch = useDispatch();
* useEffect(() => {
* dispatch(cartActions.add({ id: 1 }))
* }, [dispatch]);
* \```
*
* See: https://react-redux.js.org/next/api/hooks#usedispatch
*/
export const cartActions = cartSlice.actions;
/*
* Export selectors to query state. For use with the `useSelector` hook.
*
* e.g.
* \```
Chapter 4: Bringing it all together 79
If you’re not familiar with Redux Toolkit, you’ll notice a few new utilities.
The generated code is a great start for many use cases. We do need to make a
few tweaks, as well as add additional selectors. So let’s update the slice to the
following:
libs/cart/data-access/src/lib/cart.slice.ts
Chapter 4: Bringing it all together 80
import {
createAsyncThunk,
createEntityAdapter,
createSelector,
createSlice,
EntityState,
} from '@reduxjs/toolkit';
import { ICartItem } from '@acme/shared-models';
import { checkout } from './cart-data-access';
Now that data-access has been sorted out, let’s go ahead and add our shopping
cart feature library.
Recall that --appProject installs the new feature as a route to the bookstore
application. Nx guesses what the route path should be based on your library
name. Additionally, Nx will also guess where to add the new <Link> in your app
component.
The guesses made by Nx may not be correct, so let’s make sure we have the
proper setup.
apps/bookstore/src/app/app.tsx
// ...
// ...
Next up, let’s implement our CartFeature component. We’ll keep the imple-
mentation simple by providing the following:
libs/cart/feature/src/lib/cart-feature.tsx
display: flex;
align-items: center;
padding-bottom: 9px;
margin-bottom: 9px;
border-bottom: 1px #ccc solid;
}
.description {
flex: 1;
}
.cost {
width: 10%;
}
.action {
width: 10%;
}
`;
<span className="description">{item.description}</span>
<span className="cost">${item.cost.toFixed(2)}</span>
<span className="action">
<Button onClick={() => dispatch(cartActions.remove(item.id)\
)}>
Remove
</Button>
</span>
</div>
))}
</div>
<p>Total: ${total.toFixed(2)}</p>
<Button
disabled={cartIsEmpty || status !== 'ready'}
onClick={() => dispatch(checkoutCart(cartItems))}
>
Checkout
</Button>
</>
)}
</StyledCartFeature>
);
};
Notice that we can dispatch the generated cartActions as well as the async
thunk checkoutCart through the Redux store. This is because Redux Toolkit adds
thunk support by default. The rest of the code is fairly standard Redux usage
within a React component–select state via useSelector and dispatch actions
using useDispatch.
We had previously used alert when users clicked on the Add button in the books
feature. Now that we have our cart feature ready, we can wire up this behavior
properly.
libs/cart/feature/src/lib/books-feature.tsx
useEffect(() => {
getBooks().then(setBooks);
}, []);
return (
<>
<h2>Books</h2>
<Books
books={books}
onAdd={(book) =>
// Using add action from cart slice
dispatch(
cartActions.add({
id: book.id,
description: book.title,
cost: book.price,
Chapter 4: Bringing it all together 87
})
)
}
/>
</>
);
};
Let’s look at the final result by serving up our bookstore and api apps.
Looking good! However, we’re not quite yet finished. Don’t forget about our
tests!
nx affected:test
Tests are broken, so please fix them up. If you do get stuck though, you may refer
to the solution repository: https://github.com/nrwl/nx-react-book-example.
You’ve made it this far! Now is a good time to commit our progress before
looking at production builds: git add .; git commit -m 'add cart feature''.
Chapter 4: Bringing it all together 92
Now that we have completed our features we can build the frontend and backend
apps for running in production.
nx build api
nx build bookstore
When both build succeed you will see the following output in the dist folder.
dist
└── apps
├── api
│ ├── assets
│ ├── main.js
│ └── main.js.map
└── bookstore
├── 3rdpartylicenses.txt
├── assets
├── favicon.ico
├── index.html
├── main.fc726d4f52fe3ea5.esm.js
├── main.fc726d4f52fe3ea5.esm.js.LICENSE.txt
├── polyfills.7e0034cfe0406d00.esm.js
└── runtime.bdc91b7b4b12a0bf.esm.js
You can run the backend application using Node, and the frontend application
using any static file server solution.
e.g.
Chapter 4: Bringing it all together 93
You’ll notice issues with /api not being available from the frontend app. There
are many ways to solve this, but for the sake of simplicity we will serve the
frontend app through the API server.
apps/api/src/main.ts
// ...
server.on('error', console.error);
nx build api
node dist/apps/api/main.js