• Google Developer Expert (GDE) in Angular
  • Author of Mastering Angular Reactive Forms
  • Educator & Technical Content Creator
  • Senior Angular Developer  @ ASI
  • Co-organizer of Angular Athens Meetup

Fanis Prodromou

Code. Teach. Community. Angular.

https://blog.profanis.me

/prodromouf

@prodromouf

Scalable Architecture & Modern Reactivity

profanis

Target Audience & Prerequisites

profanis

Target Audience

Ready to modernize workflows and move away from "spaghetti" monoliths.

Forward-Thinking Engineers

Familiar with Core-Feature-Shared but ready for a more scalable approach.

Architecture Seekers

1+ years of professional experience with the core framework.

Experienced Angular Developers

Target Audience & Prerequisites

profanis

What You DON'T Need to Know

Learn how to simplify your code and reduce boilerplate with Signal patterns.

No Mastery of RxJS Required

We will cover the mental shift from Zone.js to fine-grained reactivity.

No Prior Signals Knowledge Needed

We’ll learn the CLI and workspace structure from the ground up.

No Prior Nx Experience Required

🧑‍💻 Nx Foundation & Architecture

90 mins

Short Break

15 mins

🧑‍💻 Nx Architecture & the Intelligent Graph 

90 mins

🍌 Short Break

15 mins

🧑‍💻 Introducing Signals

90 mins

🧑‍🍳 Lunch Break

45 mins

🧑‍💻 Advanced Reactivity

90 mins

🍌 Short Break

15 mins

🙋 Wrap up and Q&A

30 mins

profanis

Nx Foundation & Architecture

Building Scalable "Moduliths" with Angular

profanis

  • The "Monolith" Problem
  • Why Nx?
  • What is "The Modulith"?
  • Explore the folder structure
  • Project Types
  • The Golden Rule: The Barrel File
  • Tooling: CLI & Nx Console
  • Hands-on Overview

In this lesson

profanis

The Goal:

profanis

GreenHeaven Plant Shop

profanis

The Goal:

Products List

Product Details

Infinite Scroll

Search Functionality

tbd...

profanis

Show Time

profanis

profanis

We aren't here to just learn a new library.

 

We are here to fix the way we work.

profanis

The Angular Developer’s "Wall

Daily frustrations we all face

Fearing circular dependencies and accidental coupling when sharing code.

The "Spaghetti" Fear

Debating folder structures because the standard CLI lacks a roadmap for scale.

Architecture Debt

Running 1,000 tests when you only touched two files.

Wasted Effort

Why Nx?

A smart build system  (like Angular CLI on steroids)

Modern Tooling

Architectural Guardrails

Smart Rebuilds ("Affected")

Computation Caching

profanis

Why Nx?

A smart build system  (like Angular CLI on steroids)

Computation Caching

profanis

nx test

Why Nx?

A smart build system  (like Angular CLI on steroids)

Smart Rebuilds ("Affected")

profanis

nx build --affected

Why Nx?

A smart build system  (like Angular CLI on steroids)

Architectural Guardrails

profanis

nx lint

Why Nx?

A smart build system  (like Angular CLI on steroids)

Modern Tooling

profanis

The "Monolith" Problem

Reusing code between a Customer App and an Admin App is difficult.

Hard to Share

Compiles one project at a time

Project Centric

Features accidentally depend on each other

Tight Coupling

Dump everything into src/app

profanis

The "Monolith" Problem

src/
└── app/
    ├── core/
    │   ├── guards/
    │   │   ├── auth.guard.ts
    │   │   └── auth.guard.spec.ts
    │   ├── header/
    │   │   ├── header.component.ts
    │   │   ├── header.component.html
    │   │   └── header.component.scss
    │   ├── core.service.ts
    │   └── core.service.spec.ts
    ├── features/
    │   ├── feature-1/
    │   │   ├── feature-1.component.ts
    │   │   ├── feature-1.component.html
    │   │   ├── feature-1.routes.ts
    │   │   └── feature-1.service.ts
    │   └── feature-2/
    │       ├── feature-2.component.ts
    │       ├── feature-2.component.html
    │       ├── feature-2.routes.ts
    │       └── feature-2.service.ts
    ├── shared/
    │   ├── components/
    │   │   ├── button/
    │   │   │   ├── button.component.ts
    │   │   │   ├── button.component.html
    │   │   │   └── button.component.scss
    │   │   └── input/
    │   │       ├── input.component.ts
    │   │       ├── input.component.html
    │   │       └── input.component.scss
    │   ├── guards/
    │   │   ├── can-leave.guard.ts
    │   │   └── can-leave.guard.spec.ts
    │   └── pipes/
    │       ├── format-date.pipe.ts
    │       └── format-date.pipe.spec.ts
    ├── app.component.ts
    ├── app.component.html
    ├── app.routes.ts
    ├── app.config.ts
    └── main.ts

dump everything into src/app

profanis

The "Monolith" Problem

src/
└── app/
    ├── core/
    │   ├── guards/
    │   │   ├── auth.guard.ts
    │   │   └── auth.guard.spec.ts
    │   ├── header/
    │   │   ├── header.component.ts
    │   │   ├── header.component.html
    │   │   └── header.component.scss
    │   ├── core.service.ts
    │   └── core.service.spec.ts
    ├── features/
    │   ├── feature-1/
    │   │   ├── components/
    │   │   │   ├── component-a.ts
    │   │   │   └── component-b.ts
    │   │   ├── services/
    │   │   │   ├── service-a.ts
    │   │   │   └── service-b.ts
    │   │   ├── pipes/
    │   │   │   ├── pipe-a.ts
    │   │   │   └── pipe-b.ts
    │   │   ├── feature-1.component.ts
    │   │   ├── feature-1.component.html
    │   │   ├── feature-1.routes.ts
    │   │   └── feature-1.service.ts
    │   └── feature-2/
    │       ├── feature-2.component.ts
    │       ├── feature-2.component.html
    │       ├── feature-2.routes.ts
    │       └── feature-2.service.ts
    ├── app.component.ts
    ├── app.component.html
    ├── app.routes.ts
    ├── app.config.ts
    └── main.ts

dump everything into src/app

profanis

The "Monolith" Problem

Features accidentally depend on each other

Tight Coupling

// src/app/core/logger.service.ts

import { FeatureAServiceService } 
	from '../features/feature-a.service'; // <-- Oops

@Injectable({ providedIn: 'root' })
export class LoggerService {
  private featureService = inject(FeatureAService)
}
// src/app/feature-1.service.ts
import { Feature2Service } 
	from '../feature-2.service'; // Dependency A -> B

@Injectable()
export class Feature1Service {
}

// src/app/feature-2.service.ts
import { Feature1Service } 
	from '../feature-1.service'; // Dependency B -> A

export class Feature2Service {
}

profanis

The "Monolith" Problem

Compiles one project at a time

Project Centric

profanis

The "Monolith" Problem

Reusing code between a Customer App and an Admin App is difficult.

Hard to Share

profanis

The modulith

You don't need 50 apps to use Nx

Concept

- We build a Single Application
Monolith deployment

- We structure it like Microservices 
Modular development

profanis

my-workspace/
├── apps/
│   └── main-app/
│       ├── app.component.ts
│       ├── app.config.ts
│       ├── app.routes.ts
│       └── main.ts
└── libs/
    ├── auth/
    │   └── guards/
    │       ├── auth.guard.ts
    │       └── auth.guard.spec.ts
    ├── shared/
    │   ├── ui/
    │   │   └── header/
    │   │       ├── header.component.ts
    │   │       ├── header.component.html
    │   │       └── header.component.scss
    │   └── data-access/
    │       ├── core.service.ts      
    │       └── core.service.spec.ts
    ├── feature-1/
    │   ├── components/
    │   ├── services/
    │   ├── pipes/
    │   ├── feature-1.component.ts
    │   ├── feature-1.routes.ts
    │   └── index.ts                 
    └── feature-2/
        ├── feature-2.component.ts
        ├── feature-2.routes.ts
        └── index.ts

The modulith

You don't need 50 apps to use Nx

Strategy

- Treat your libs folder like internal npm packages

- Strict boundaries between features

- Clear public APIs

profanis

The modulith

You don't need 50 apps to use Nx

Why

Lack of organization slows down engineers

profanis

Folder Structure

/apps 

- The shell

- Entry point that wires everything up

/libs

- business logic

- components

- UI components

📂 workspace-root/
├── 📂 apps/
│   ├── 📂 customer-portal/  <-- App Shell
└── 📂 libs/
    ├── 📂 data-access/      <-- Shared Logic
    ├── 📂 ui-components/    <-- Shared UI
    └── 📂 feature-orders/   <-- Feature Logic

profanis

Folder Structure

/apps 

20%

/libs

📂 workspace-root/
├── 📂 apps/
│   ├── 📂 customer-portal/  <-- App Shell
└── 📂 libs/
    ├── 📂 data-access/      <-- Shared Logic
    ├── 📂 ui-components/    <-- Shared UI
    └── 📂 feature-orders/   <-- Feature Logic

80%

profanis

Project Types

Utility

- Like Date Formatters

Data-Access

- Like our Product API Service

UI

- Like our Product Card

The structure is the solution

Feature

- Like our Catalogue Page

profanis

my-workspace/
├── apps/
│   └── main-app/
│       ├── app.component.ts
│       ├── app.config.ts
│       ├── app.routes.ts
│       └── main.ts
└── libs/
    ├── auth/
    │   └── guards/
    │       ├── auth.guard.ts
    │       └── auth.guard.spec.ts
    ├── shared/
    │   ├── ui/
    │   │   └── header/
    │   │       ├── header.component.ts
    │   │       ├── header.component.html
    │   │       └── header.component.scss
    │   └── data-access/
    │       ├── core.service.ts      
    │       └── core.service.spec.ts
    ├── feature-1/
    │   ├── components/
    │   ├── services/
    │   ├── pipes/
    │   ├── feature-1.component.ts
    │   ├── feature-1.routes.ts
    │   └── index.ts                 
    └── feature-2/
        ├── feature-2.component.ts
        ├── feature-2.routes.ts
        └── index.ts

The Barrel File

Only Export What You Mean To Share

The Problem

- developers can import any file deep within another library

import { InternalHelper } 
	from 'libs/data-access/src/lib/internal-folder/helper.service'

profanis

The Barrel File

Only Export What You Mean To Share

The Solution

- Every Nx library has a barrel file, typically located at libs/my-lib/src/index.ts

- Anything you don't export from this file is considered "private" to that library.

profanis

The Barrel File

Only Export What You Mean To Share

Example

📂 libs/data-access/
├── 📂 src/lib/
│   ├── internal-helper.service.ts  <-- Private file
│   └── public-product.service.ts   <-- Public file
└── index.ts                        <-- Public API
// index.ts content:
export * from './src/lib/public-product.service';

// We do NOT export internal-helper.service.ts here!

profanis

The How

NX Tooling

Powerful CLI

# Create a feature library
npx nx g @nx/angular:library --name=feature-home 
							 --directory=libs/feature-home
                             
# Create a UI library
npx nx g @nx/angular:library --name=ui-header 
							 --directory=libs/shared/ui/header

profanis

Create a workspace

 

 

Create a Library

 

Generate a Component

Basic NX Commands

CLI

more: https://nx.dev/docs/technologies/angular/guides/nx-and-angular

              https://nx.dev/docs/reference/nx-commands

profanis

npx create-nx-workspace@latest
nx generate @nx/angular:library [name]
nx generate @nx/angular:component [name] --project=[project-name]

The How

NX Tooling

NX Console

profanis

profanis

profanis

profanis

profanis

Hands-on (slide 40)

- Create the Nx Workspace

- Install the Nx Console plugin in VS Code/WebStorm.

- Generate the feature-catalogue using NX CLI

- Generate the feature-home using the NX Console

- Setup the router

profanis

Hands-on

Create the NX Workspace

npx create-nx-workspace@latest --preset angular-monorepo

Hint

profanis

Hands-on

 Where would you like to create your workspace? · greenHeaven

 Application name · greenHeaven

 Which bundler would you like to use? · esbuild

 Default stylesheet format · scss

 Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? · No

 Which unit test runner would you like to use? · jest

 Test runner to use for end to end (E2E) tests · none

 Try the full Nx platform? · skip

 Which CI provider would you like to use? · skip

 Would you like remote caching to make your build faster? · skip

Create the NX Workspace

profanis

Hands-on

Install the Nx Console plugin in VS Code

profanis

Hands-on

Setup & Exploration

Explore the file system

and identify the:

- apps/greenHeaven/project.json

- nx.json

profanis

Hands-on

Create the Products-List Feature using the NX CLI

npx nx g @nx/angular:library --name=feature-catalogue 
							 --directory=libs/feature-catalogue

Hint

profanis

Hands-on

Create the Home Feature using the NX Console

Hint

profanis

- right click on the libs directory

- click the Nx Generate (UI)

Hands-on

Create the Home Feature using the NX Console

profanis

- select @nx/angular - library

Hands-on

Create the Home Feature using the NX Console

Hint

profanis

Hands-on

Verify the barrel files and the project.json

- Open the index.ts of both libraries and verify the components are exported.

- Open the project.json of each library and identify the name of the library and the projectType

profanis

Hands-on

Create the Routing

// apps/greenHeaven/src/app/app.routes.ts

export const appRoutes: Route[] = [
  {
    path: '',
    loadComponent: () =>
      import('@green-heaven/feature-home').then((m) => m.FeatureHome),
    pathMatch: 'full',
  },
  {
    path: 'catalogue',
    loadComponent: () =>
      import('@green-heaven/feature-catalogue').then((m) => m.FeatureCatalogue),
  },
];

profanis

Hands-on

Create the Routing

- Update app.html  to include a simple navbar (just text links for now)

- npx nx serve greenHeaven

- Navigate between "Home" and "Catalogue"

profanis

Hands-on

Create the Routing

Ensure the Feature is Rendered

profanis

Nx Architecture &

The Intelligent Graph

Types, Scopes, and the Shell Pattern

profanis

  • Project Types (The Building Blocks)
  • Grouping by Scope (The Domain)
  • The Shell Library Pattern
  • Naming Conventions
  • Metadata & Tags (project.json)
  • The Intelligent Graph

profanis

In this lesson

Project Types

State (Signals), NGXS/NGRX, HTTP Services

Data-Access (type:data-access)

Dumb components. Pure presentation

UI (type:ui)

Pure functions, helpers, validators

Utility (type:util)

The building blocks

The "Smart" logic and routing.

Feature (type:feature)

profanis

Feature Libraries

type:feature

Rules

It’s the only library type allowed to have routes.

profanis

The role

Smart components that orchestrate the UI components.

GreenHeaven example

feature-catalogue

What it does

Orchestrates the entire page

Contains

Domain-specific logic

Page Layouts

Services / Facades

UI Libraries

type:ui

GreenHeaven example

ui-product-card

Rules

It just receives input(s) and emits output(s)

profanis

The role

"Dumb" components focused on pure presentation.

Contains

Cards

Business UI Components

Buttons

What it does

A UI unit of the entire application

Data-Access Libraries

type:data-access

GreenHeaven example

catalogue-data-access

profanis

The role

Managing State and API communication.

Contains

HTTP Services

NGXS/NGRX

Angular Signals/State

What it does

Communicates with the HTTP API

Rules

It has no UI components

Utility Libraries

type:util

profanis

The role

Low-level, pure helper functions

Contains

Date formatters

Math helpers

Custom validators

GreenHeaven example

-

What it does

Increases the reusability

Rules

Has zero dependencies on other library types.

Do I need a library?

Q: "Where do I put this file?"

Which project  owns  this responsibility?

Do I need a library?

A: Yes -> create a feature library

A: Yes -> create a UI library

A: Yes -> create a data-access library

Q: "I should develop a new pageable component"

Q: "I should develop a reusable UI element

Q: "I should develop a state management slice

src/
└── app/
    ├── core/
    │   ├── guards/
    │   │   ├── auth.guard.ts
    │   │   └── auth.guard.spec.ts
    │   ├── header/
    │   │   ├── header.component.ts
    │   │   ├── header.component.html
    │   │   └── header.component.scss
    │   ├── core.service.ts
    │   └── core.service.spec.ts
    ├── features/
    │   ├── feature-1/
    │   │   ├── components/
    │   │   │   ├── component-a.ts
    │   │   │   └── component-b.ts
    │   │   ├── services/
    │   │   │   ├── service-a.ts
    │   │   │   └── service-b.ts
    │   │   ├── pipes/
    │   │   │   ├── pipe-a.ts
    │   │   │   └── pipe-b.ts
    │   │   ├── feature-1.component.ts
    │   │   ├── feature-1.component.html
    │   │   ├── feature-1.routes.ts
    │   │   └── feature-1.service.ts
    │   └── feature-2/
    │       ├── feature-2.component.ts
    │       ├── feature-2.component.html
    │       ├── feature-2.routes.ts
    │       └── feature-2.service.ts
    ├── app.component.ts
    ├── app.component.html
    ├── app.routes.ts
    ├── app.config.ts
    └── main.ts

Q: "Do I need a library even for a component?"

A: No -> the component is an implementation detail

The feature library is allowed to be "fat" internally

Use the index.ts to hide the complexity

Scaling Up

Organizing by Domain

As the app grows, a flat libs/ folder with 100+ libraries becomes hard to find code and hard to see ownership

The problem

Group libraries by Scope (Domain)

The solution

profanis

App

Util

Data

UI

Feat

UI

UI

Feat

Data

UI

UI

Util

UI

Util

Feat

Feat

Feat

Feat

Feat

profanis

App

Util

Data

UI

Feat

UI

UI

Feat

Data

UI

UI

Util

UI

Util

Feat

Feat

Feat

Feat

Feat

Scope A

Scope B

Scope C

profanis

App

Util

Data

UI

Feat

UI

UI

Feat

Data

UI

UI

Util

UI

Util

Feat

Feat

Feat

Feat

Feat

Scope A

Scope B

Scope C

profanis

The Shell Library

The Entry Point for a domain

If the App knows about "List" and "Details", the App is too smart

The problem

The App knows the Shell.

The Shell sets up the child routes for the domain

The solution

profanis

App

Util

Data

UI

Feat

UI

UI

Feat

Data

UI

UI

Util

UI

Util

Feat

Feat

Feat

Feat

Feat

Scope A

Scope B

Scope C

profanis

App

Util

Data

UI

Feat

UI

UI

Feat

Data

UI

UI

Util

UI

Util

Feat

Feat

Feat

Feat

Feat

Scope A

Scope B

Scope C

Shell

Shell

Shell

profanis

App

Util

Data

UI

Feat

UI

UI

Feat

Data

UI

UI

Util

UI

Util

Feat

Feat

Feat

Feat

Feat

Scope A

Scope B

Scope C

Shell

Shell

Shell

profanis

App

Util

Data

UI

Feat

UI

UI

Feat

Data

UI

UI

Util

UI

Util

Feat

Feat

Feat

Feat

Feat

Scope A

Scope B

Scope C

Shell

Shell

Shell

profanis

The Shell Library

It orchestrates how the work is started and displayed.

A specific sidebar, header, or footer that only appears within this feature module

Wrapper Layouts

Provides NGXS/NgRx state slices for the entire domain.

State Bootstrapping

Handle the auth guards and route resolvers

Guards and Resolvers

Defines the top-level routes that the main app loads. 

Routing & Lazy Loading

profanis

App

Util

Data

UI

Feat

UI

UI

Feat

Data

UI

UI

Util

UI

Util

Feat

Feat

Feat

Feat

Feat

Scope A

Scope B

Scope C

Shell

Shell

Shell

profanis

The Shell Library

The Entry Point for a domain

// src/app/routes.ts

export const appRoutes: Route[] = [
  { path: '', pathMatch: 'full', redirectTo: 'home' },
  {
    path: 'catalogue',
    loadComponent: () =>
      import('@workshop/catalogue-feature-catalogue-list').then(
        (m) => m.CatalogueComponent
      ),
  },
  {
    path: 'catalogue/:id',
    loadComponent: () =>
      import('@workshop/catalogue-feature-catalogue-details').then(
        (m) => m.CatalogueDetailsComponent
      ),
  },
  { path: '**', redirectTo: '' },
];

profanis

The Shell Library

The Entry Point for a domain

// src/app/routes.ts

export const appRoutes: Route[] = [
  { path: '', pathMatch: 'full', redirectTo: 'home' },
  {
    path: 'catalogue',
    loadChildren: () =>
      import('@workshop/catalogue-feature-shell').then(
        (m) => m.catalogueRoutes
      ),
  },
  { path: '**', redirectTo: '' },
];

profanis

App

Scope A

Scope B

Scope C

Feat

UI

Data

Util

UI

Feat

Util

Util

Feat

UI

Data

profanis

App

UI

Data

Util

Feat

Feat

UI

Util

Util

UI

Data

Feat

Scope A

Scope B

Scope C

profanis

Metadata and Tags

How does Nx know that feature-list is a "Feature"? We tell it.

type: segregates the technical responsibility 

scope: segregates the functional responsibility

profanis

Metadata and Tags

How does Nx know that feature-list is a "Feature"? We tell it.

// project.json

{
  "name": "catalogue-feature-catalogue-details",
  "$schema": "../../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "libs/catalogue/feature-catalogue-details/src",
  "prefix": "lib",
  "projectType": "library",
  "tags": ["scope:catalogue", "type:feature"],
  "targets": {
    "test": { ... },
    "lint": { ... }
  }
}

profanis

Scope A

Util

Feat

UI

Data

Metadata and Tags

How does Nx know that feature-list is a "Feature"? We tell it.

profanis

Metadata and Tags

How does Nx know that feature-list is a "Feature"? We tell it.

// eslint.config.mjs

{
	"sourceTag": "type:feature",
	"onlyDependOnLibsWithTags": [
		"type:ui",
		"type:util",
		"type:feature",
		"type:data-access"
	]
},
{
	"sourceTag": "type:ui",
	"onlyDependOnLibsWithTags": [
		"type:util",
		"type:ui",
	]
}

profanis

Metadata and Tags

How does Nx know that feature-list is a "Feature"? We tell it.

profanis

scope: catalogue

scope: shared

UI

Data

Util

Metadata and Tags

How does Nx know that feature-list is a "Feature"? We tell it.

Feat

Util

Util

UI

Util

Util

UI

profanis

Scaling Up

Organizing by Domain

Within a single domain, we apply our Taxonomy to keep the internal structure clean

libs/
└── catalogue/                      <-- The Domain Boundary
    ├── data-access/                🧠 State & API 
    ├── feature-catalogue-details/  📄 Catalogue Details Page 
    ├── feature-catalogue-list/   	📄 Catalogue List Page 
    ├── feature-shell/     			🐚 Entry Point & Internal Routing
    └── types/     					🐚 Domain specific types

profanis

// project.json

{
  "name": "catalogue-feature-catalogue-details",
  "$schema": "../../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "libs/catalogue/feature-catalogue-details/src",
  "prefix": "lib",
  "projectType": "library",
  "tags": ["scope:catalogue", "type:feature"],
  "targets": {
    "test": { ... },
    "lint": { ... }
  }
}

Naming Convention

Naming consistency is key for tools and humans

profanis

Naming Convention

Naming consistency is key for tools and humans

Data

Util

UI

scope: users

Data

Util

UI

scope: catalogue

product-list

product-list

profanis

Naming Convention

Naming consistency is key for tools and humans

Data

Util

UI

scope: users

Data

Util

UI

scope: catalogue

product-list

product-list

product-list

product-list

profanis

scope-type-identifier*

catalogue
shared
auth

feature
ui
data-access

catalogue-list
header
users*

Naming Convention

profanis

scope-type-identifier*

catalogue-feature-catalogue-list
shared-ui-header
auth-data-access-users

Naming Convention

profanis

scope-type-identifier*

catalogue-feature-catalogue-list
shared-ui-header
auth-data-access-users

// project.json

{
  "name": "catalogue-feature-catalogue-list",
  "$schema": "../../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "libs/catalogue/feature-catalogue-list/src",
  "prefix": "lib",
  "projectType": "library",
  ....
}

Naming Convention

profanis

scope-type-identifier*

catalogue-feature-catalogue-list
shared-ui-header
auth-data-access-users

@org/catalogue-feature-catalogue-list
@org/shared-ui-header
@org/auth-data-access-users

Naming Convention

profanis

scope-type-identifier*

catalogue-feature-catalogue-list
shared-ui-header
auth-data-access-users

@org/catalogue-feature-catalogue-list
@org/shared-ui-header
@org/auth-data-access-users

// tsconfig.base.json

{
  "compilerOptions": {
    "paths": {
      "@org/catalogue-feature-catalogue-list": [
        "libs/catalogue/feature-catalogue-list/src/index.ts"
      ],
      "@org/shared-ui-header": [
      	"libs/shared/ui/ui-header/src/index.ts"
      ],
      "@org/auth-data-access-users": [
      	"libs/auth/data-access/src/index.ts"
      ]
    }
  }
}

Naming Convention

profanis

The Intelligent Graph

Visualize your architecture

Defines the top-level routes that the main app loads. 

nx graph

Visualize only the affected by your changes projects

nx graph --affected

Visualize only the affected by your changes projects - CI

nx affected -t build --base=origin/main~1 --head=origin/main

profanis

The Intelligent Graph

Visualize if your architecture is messy > nx graph

profanis

The Intelligent Graph

Visualize if your architecture is messy > nx graph --affected

profanis

The Intelligent Graph

Visualize if your architecture is messy > detect an error

profanis

Hands-on (slide 97)

- Create a "shell" library for the Catalogue

- Create a "shell" library for the Home

- Create a catalogue data-access library

- Create a ui product-card library  

- Identify and solve the problem with the product.ts

profanis

Hands-on

Start Fresh

Preparation

git clone --branch lab/one https://github.com/profanis/nx-signals-workshop.git
cd nx-signals-workshop/
npm i

Let me guide you through...

profanis

Hands-on

 Create a "shell" library for the Catalogue 1/3

Hint

npx nx g @nx/angular:library --directory=libs/catalogue/feature-shell --name=catalogue-feature-shell

- Use the Nx CLI

profanis

- Delete the auto-generated component

- Create the lib-routes.ts file under lib dir

Hands-on

 Create a "shell" library for the Catalogue 2/3

Hint

// lib.routes.ts

export const catalogueRoutes: Route[] = [
  {
    path: '',
    // ??
  },
  {
    path: ':id',
    // ??
  },
];

profanis

Hands-on

 Create a "shell" library for the Catalogue 3/3

Hint

- Update the app/app.routes.ts

// app/app.routes.ts

{
    path: 'catalogue',
    loadChildren: () =>
      import('PATH TO FEATURE SHELL').then(
        (m) => m.ROUTES
      ),
  },

profanis

(note: locate the path of the library in the tsconfig.base.json)

Hands-on

Create a "shell" library for the Home

Hint

- Update the app/app.routes.ts

  {
    path: 'home',
    loadChildren: () =>
      import('PATH TO FEATURE SHELL').then(
         (m) => m.ROUTES
      ),
  }

- Use the NX console and give the correct project name

profanis

Hands-on

Create a catalogue data-access library

- Move the file from 

libs/catalogue/feature-catalogue-list/src/lib/services/products.api.ts 

to data-access library

profanis

Hands-on

Create a ui library for the product-card

- Move the file from 

libs/catalogue/feature-catalogue-list/src/lib/components/product-card.component.ts

to ui-product-card

profanis

Hands-on

lint all - problem

npx nx run-many -t lint

Run the command

it will throw...

...

Linting "home-feature-home"...

/libs/home/feature-home/src/lib/home-page.component.ts

2:1   error    A project tagged with "type:feature" can only depend on libs tagged with 
 "type:ui", "type:util", "type:model", "type:feature", "type:data-access"  
 @nx/enforce-module-boundaries

profanis

Hands-on

lint all - solution

Identify the project.json of the libraries:

  • shared-ui-header
  • shared-ui-hero
  • shared-ui-product-card

 

Add the tag -> "type:ui"

npx nx run-many -t lint

Run the command again:

Identify the module boundaries in the eslint.config.mjs

profanis

Hands-on

Identify and solve the problem with the product card

Hint

nx graph

profanis

Hands-on

Identify and solve the problem with the product card -> solution

Hint

nx graph

profanis

Introducing Signals

Βridge the Structure (Nx) and State (Signals)

profanis

profanis

  • The Signals: What and Why
  • Signals API

In this lesson

- smart variables that notify anyone who's interested when their value changes.

What are Signals?

- Traditional change detection checks everything. Signals allow tracks specific values

profanis

- Traditional Change Detection can be slow on complex applications

Why Signals?

- zone.js is great but triggers the CD multiple times 

profanis

Zone.js

User Interaction

dom event (click)

zone.js

(Angular Zone)

Change Detection

UI Update

profanis

profanis

Angular checks the entire component tree when the micro-task queue is empty

profanis

Default + Observable

OnPush + Observable

OnPush + Signals

profanis

OnPush + Observable

OnPush + Signals

profanis

OnPush + Signals

profanis

profanis

Observers

Subject

Consumers

Producers

profanis

Template

counter

Consumers

Producers

profanis

counter = signal<number>(0);

Producer

profanis

counter = signal<number>(0);

Returns a WritableSignal

Producer

profanis

counter = signal<number>(0);

Define the type

Producer

profanis

counter = signal<number>(0);

Default value

Producer

profanis

<div>
  {{ counter() }}
</div>

Consumer

Template Context

Consumer

profanis

const evenOrOdd = 
      computed(() => counter() % 2 === 0 ? 'even' : 'odd');

Consumer & Producer

Producer

Consumer

profanis

computed

profanis

const derivedState = 
         computed(() => mySignalArray().length);

- The derivedState is getting updated when the source signal has a change

* source signal = mySignalArray()

Computed

profanis

effect

profanis

constructor() {
   effect(() => {
      console.log(mySignalArray().length)
   })
}

- The effect runs synchronously

- The effect registers the referenced signals as dependencies.

Effect

- Logging

- DOM Manipulation

- Storage handling (e.g. localStorage)

- Update other signals*

Use for

profanis

equality

profanis

mySignalArray = signal([], { equal: _.isEqual })

constructor() {
   effect(() => {
      console.log(mySignalArray().length)
   })
}

- by default signals use the Object.is() comparison

- optionally provide an equality function

Equality

profanis

signal

inputs

profanis

@Component({...})
export class MyComponent {
  @Input() isChecked = false;
}

profanis

@Component({...})
export class MyComponent {
  isChecked = input(false);
}

read-only signal

profanis

export interface UserModel {
  name: string;
  age: number;
  
  /*Social*/
  address: string;
  twitter: string;
  linkedin: string;
  github: string;
  instagram: string;
  facebook: string;
  website: string;
  email: string;
}

profanis

@Component({...})
export class MyComponent implements OnChanges {
  @Input({ required: true }) user!: UserModel;
  userSocials: string[] = [];

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.user) {
      const { name, age, ...userSocials } = this.user;
      this.userSocials = Object.values(userSocials);
    }
  }
}

profanis

@Component({...})
export class MyComponent {
  user = input.required<UserModel>();
  userSocials = computed(() => {
    const { name, age, ...userSocials } = this.user();
    return Object.values(userSocials);
  });
}

profanis

model input

profanis

@Component({...})
export class ChildComponent {
  @Input({ required: true }) name!: string;
  @Output() nameChange = new EventEmitter<string>();
}

profanis

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child 	[name]="username" 
				(nameChange)="changeHandler($event)" />
  `,
})
export class ParentComponent { 
   username = 'profanis';
}

profanis

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child 	[name]="username" 
				(nameChange)="changeHandler($event)" />
  `,
})
export class ParentComponent { 
   username = 'profanis';
}

profanis

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: ` <app-child [(name)]="username" /> `,
})
export class ParentComponent {
  username = 'profanis';
}

profanis

@Component({...})
export class ChildComponent {
  // @Input({ required: true }) name!: string;
  // @Output() nameChange = new EventEmitter<string>();
  name = model<string>();
}

profanis

@Component({...})
export class ChildComponent {
  // @Input({ required: true }) name!: string;
  // @Output() nameChange = new EventEmitter<string>();
  name = model.required<string>();
}

profanis

@Component({...})
export class ChildComponent {
  name = model<string>(); // writable signal

  addTitle() {
    this.name.update((name) => `Mr. ${name}`);
  }
}

profanis

@Component({...})
export class ChildComponent {
  name = model<string>(); // writable signal

  titleExists = computed(() => this.name().startsWith('Mr.'));
}

profanis

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: ` <app-child [(name)]="username" /> `,
})
export class ParentComponent {
  username = 'profanis';
}

profanis

new output

profanis

@Component({...})
export class ChildComponent {
  name = input.required<string>();
  @Output() nameChange = new EventEmitter<string>();
}

profanis

@Component({...})
export class ChildComponent {
  name = input.required<string>();
  nameChange = output<string>()
}

profanis

@Component({...})
export class ChildComponent {
  @Output() formIsValid = this.form.statusChanges.pipe(
     map((status) => status === 'VALID'),
  );
}

profanis

import { outputFromObservable } from '@angular/core/rxjs-interop';

@Component({...})
export class ChildComponent {
  formIsValid = outputFromObservable(
    this.form.statusChanges.pipe(
            map((status) => status === 'VALID'))
  );
}

profanis

import { outputToObservable } from '@angular/core/rxjs-interop';

@Component({...})
export class ChildComponent {
  name = input.required<string>();
  nameChange = output<string>()
  nameChange$ = outputToObservable(nameChange)
}

profanis

profanis

view queries

@Component({
  selector: 'app-parent',
  template: ` <div #child>Child wrapper</div> `,
})
export class ParentComponent {
  child = viewChild('child', { read: ElementRef<HTMLElement> });

  constructor() {
    effect(() => {
      const child = this.child();
      if (child) {
        console.log('Child element:', child.nativeElement);
      }
    });
  }
}

profanis

@Component({
  selector: 'app-parent',
  template: ` <div #child>Child wrapper</div> `,
})
export class ParentComponent {
  child = viewChild.required('child', { read: 
                                       	ElementRef<HTMLElement> });

  constructor() {
    effect(() => {
      const child = this.child();
      console.log('Child element:', child.nativeElement);
    });
  }
}

profanis

<app-parent>
    <app-child></app-child>
</app-parent>

profanis

@Component({
  selector: 'app-parent',
  imports: [ChildComponent],
})
export class ParentComponent {
  child = contentChild(ChildComponent);

  constructor() {
    effect(() => {
      const childComponent = this.child();
    });
  }
}
<app-parent>
    <app-child></app-child>
</app-parent>

profanis

@Component({
  selector: 'app-parent',
  imports: [ChildComponent],
})
export class ParentComponent {
  child = contentChild.required(ChildComponent);

  constructor() {
    effect(() => {
      const childComponent = this.child();
    });
  }
}
@Component({ })
export class ChildComponent {
  buttonClick = output<void>();

  onButtonClick() {
    this.buttonClick.emit();
  }
}

profanis

export class ParentComponent {
  child = contentChild(ChildComponent);

  constructor() {
    effect(() => {
      const childComponent = this.child();

      if (childComponent) {
        const buttonClick$ = outputToObservable(childComponent.buttonClick);

        buttonClick$.subscribe(() => {
          console.log('Child button was clicked!');
        });
      }
    });
  }
}

Hands-on

- Stop calling a function in the template. Use computed

- Create a filter component and use signal input/output

- Manage Favorite Products

profanis

Hands-on (slide 162)

Stash your changes

Preparation

git stash push -u -m "my changes: lab two"

Checkout

git checkout lab/two

Let me guide you through...

profanis

Hands-on

Stop calling a function in the template. Use computed

1. Locate the product-details.component.ts
 

2. The getStarArray function is currently called in the template. This is inefficient. Refactor it to use a computed signal.

Create a computed signal called commentsView that derives the comments with stars from the existing data.


4. Update the template

profanis

Hands-on

Create a filter component and use signal input/output

1. Locate the plant-filter.component.ts

 

2. Define a searchTerm input (searchTerm = input<string>())
 

3. Define an output for when the user types.


4. Update the catalogue.component.html and replace the plain input with this component

profanis

Hands-on

Manage Favorite Products 1/2

1. Locate the favorites.state.ts

2. Define a private writable signal to hold the list of favorite product IDs

3. Expose a computed signal count that returns the number of favorites
4. Expose a method isFavorite(id: string) that returns  boolean for a specific ID

5. Implement toggleFavorite(id: string) to update the state

profanis

Hands-on

Manage Favorite Products 2/2

6. Inject the FavoritesState in the catalogue.component.ts and create a computed property that returns the products and the correct isFavorite boolean value

7. Inject the FavoritesState in the app.ts and use the favoriteCount as badge

profanis

Advanced Reactivity

Resources, I/O & Forms

profanis

profanis

  • rxResource and httpResource
  • LinkedSignal
  • Signals Graph
  • Signals Pull Push algorithm

In this lesson

data fetching

profanis

profanis

signal<string>

profanis

signal<string>

profanis

signal<string>

cancel

cancel

profanis

How about effect?

profanis

time - 0

time - 1

request

request

100ms

500ms

profanis

time - 0

time - 1

request

request

100ms

500ms

profanis

time - 0

time - 1

request

request

100ms

500ms

profanis

time - 0

time - 1

request

request

100ms

500ms

profanis

Sync

Async

At some point we will have the data

profanis

At some point we will have the data

We will always have data

profanis

http

isLoading

error

data

profanis

http

isLoading()

error()

data()

profanis

httpResource

profanis

httpResource makes a reactive HTTP request and exposes the request status and response value

profanis

httpResource(
	?, 
	?
)

profanis

httpResource(
	string | object | function , 
	?
)

profanis

httpResource(
	string | object | function , 
	options
)

profanis

// String
httpResource(`https://api.com/${signalValue()}`)

dependency

profanis

// Object
httpResource(
	{
		url: `https://api.com/${signalValue()}`,
		method: 'GET',
		params: { type: `${queryParamSignalValue()}` }
	}
)

dependency

profanis

// Object
httpResource(
	{
		url: `https://api.com/${signalValue()}`,
		method: 'GET',
		params: { type: `${queryParamSignalValue()}` }
	}
)

dependency

profanis

// Object
httpResource(
	{
		url: `https://api.com/${signalValue()}`,
		method: 'GET',
		params: { type: `${queryParamSignalValue()}` }
	}
)

Http verb

profanis

// Function
httpResource(() => `https://api.com/${signalValue()}`)

profanis

// Function
httpResource(() => 
             signalValue() ? 
             `https://api.com/${signalValue()}` : 
             undefined
)

profanis

// String with Options
httpResource(`https://api.com/${signalValue()}`, {
	defaultValue: {},
	parse: (response) => zodSchema.parse(response),
})

profanis

// String
resource = httpResource(`https://api.com/${signalValue()}`)
@if (resource.isLoading()) {
    <div>Loading...</div>
}

@if (resource.error()) {
    <div>Oops...</div>
}

@if (resource.value()) {
    <div>{{ resource.value() }}</div>
}

profanis

// String
resource = httpResource(`https://api.com/${signalValue()}`)
derivedState = computed(
  () => resource.value().map(it => it.title)
)

profanis

resource = rxResource({
  params: () => ({
    paramName: this.paramNameAsSignal(),
  }),
  stream: ({ params }) => this.serviceApi.get(params.paramName),
});

Dependency

Service to call

profanis

linkedSignal

profanis

listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = linkedSignal(() => this.listOfItems().length);
countOfItems.set(0)
listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = computed(() => this.listOfItems().length);
countOfItems.set(0)

profanis

listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = linkedSignal(() => this.listOfItems().length);
countOfItems.set(0)
listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = linkedSignal({
    source: this.listOfItems,
    computation: (items) => items.length,
});
countOfItems.set(0)

profanis

listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = linkedSignal({
    source: this.listOfItems,
    computation: (items) => items.length, // 3
});
countOfItems.set(0)

profanis

listOfItems = signal(['item1', 'item2', 'item3', 'item4']);

countOfItems = linkedSignal({
    source: this.listOfItems,
    computation: (items) => items.length, // 4
});
countOfItems.set(0)

profanis

profanis

profanis

profanis

listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);


// keeps the selected item
selectedItem = listOfItems[0];

profanis

listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);



// an HTTP call is happening
http.get().subscribe(data => 
	this.listOfItems.set([...data])
)

profanis

profanis

listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);





selectedItem = signal<Item | null>(null);

profanis

listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);




selectedItem = linkedSignal({
  source: this.listOfItems,
  computation: (items, previous) =>
    items.find((item) => item.name === previous?.value.name),
});

profanis

The Graph

profanis

const counter = signal(0);

profanis

const counter = signal(0);

profanis

<div> {{ counter() }} </div>

profanis

<div> {{ counter() }} </div>

profanis

<div> {{ counter() }} </div>

profanis

<div> {{ counter() }} </div>
const evenOrOdd = 
	computed(() => counter() % 2 === 0 ? 'even' : 'odd');

profanis

<div> {{ counter() }} </div>
const evenOrOdd = 
	computed(() => counter() % 2 === 0 ? 'even' : 'odd');

profanis

<div> {{ counter() }} </div>
const evenOrOdd = 
	computed(() => counter() % 2 === 0 ? 'even' : 'odd');

profanis

"Since Angular knows how the data flows, can have a more fine-grained change detection"

profanis

Pull - Push

profanis

const isValid = signal(true);
const username = signal('profanis');

effect(() => {
  if (isValid() === true) {
    console.log(username());
  }
});

// Update signal values
isValid.set(false);
username.set('profanis2');

profanis

effect(() => {
  if (isValid() === true) {
    console.log(username());
  }
});

// Update signal values
isValid.set(false);
username.set('profanis2');

Consumers

Producers

isValid

effect

username

Push (notification)

profanis

effect(() => {
  if (isValid() === true) {
    console.log(username());
  }
});

// Update signal values
isValid.set(false);
username.set('profanis2');

Consumers

Producers

isValid

username

Push (notification)

effect

profanis

effect(() => {
  if (isValid() === true) {
    console.log(username());
  }
});

// Update signal values
isValid.set(false);
username.set('profanis2');

Consumers

Producers

isValid

username

Pull (value)

effect

profanis

effect(() => {
  console.log(`${isValid()} - ${username()}`);
});

isValid.set(false);
username.set('profanis2');

Consumers

Producers

isValid

username

Push (notification)

effect

profanis

Consumers

Producers

isValid

username

Push (notification)

effect(() => {
  console.log(`${isValid()} - ${username()}`);
});

isValid.set(false);
username.set('profanis2');

effect

profanis

Consumers

Producers

isValid

username

Pull (value)

Pull (value)

effect(() => {
  console.log(`${isValid()} - ${username()}`);
});

isValid.set(false);
username.set('profanis2');

effect

profanis

effect

isValid

username

profanis

Hands-on (slide 232)

Fetch data using the rxResource

- Fetch data using rxResource

- Apply a pagination

profanis

Hands-on

Stash your changes

Preparation

git stash push -u -m "my changes: lab three"

Checkout

git checkout lab/three

Let me guide you through...

profanis

Hands-on

Fetch data using rxResource

- In the catalogue.state.ts use an rxResource to get products on every page change

 

(get rid of the mockProducts)

profanis

Hands-on

Filter data using computed

- In the catalogue.state.ts convert the products to a computed property and use the searchTerm to filter the initial products

profanis

Hands-on

Apply a pagination

- In the catalogue.component.html invoke the onLoadMore

- Use a LinkedSignal

profanis

Scalable Architecture & Modern Reactivity

By Fanis Prodromou

Scalable Architecture & Modern Reactivity

Angular Signals: A Look Under the Hood and Beyond

  • 45