• 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

Atomic State Strategies for Complex Angular UIs

What is a state?

 

explain in short that a state has several categories:

- ui state

- application state

- url state

- etc (check comments)

How can we handle the state?

 

- we can use NGXS/NGRX/AKITA

- we can use SignalStore

- or any other 3rd party library that manages local-state

Do we really need a Global State Management solution?

 

- depends on what we want to achieve

- use global when you want to share data application wide...elaborate more

- use local when you want to use the state as closer to your component as it could be

If not Global, then what? Should we go to Local?

Or should we go with God Component?

The God Component

 

- explain what this is

Let's see an example - a problem to solve

- Display a page where on the left will have a filters bar and on the main panel will have a grid with results.

- We want to select a filter from the left side and provide that in the main panel

(see comments)

check the comments

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

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

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

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 {
}

The "Monolith" Problem

Compiles one project at a time

Project Centric

The "Monolith" Problem

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

Hard to Share

Why Nx?

A smart build system  (like Angular CLI on steroids)

Modern Tooling

Architectural Guardrails

Smart Rebuilds ("Affected")

Computation Caching

Why Nx?

A smart build system  (like Angular CLI on steroids)

Computation Caching

Why Nx?

A smart build system  (like Angular CLI on steroids)

Smart Rebuilds ("Affected")

Why Nx?

A smart build system  (like Angular CLI on steroids)

Architectural Guardrails

Why Nx?

A smart build system  (like Angular CLI on steroids)

Modern Tooling

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)

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

The modulith

You don't need 50 apps to use Nx

Why

Lack of organization slows down engineers

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

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%

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

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'

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.

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!

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

Create a workspace

> npx create-nx-workspace@latest

 

Create a Library
> nx generate @nx/angular:library [name]

Generate a Component

> nx generate @nx/angular:component [name] --project=[project-name]

Basic NX Commands

CLI

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

The How

NX Tooling

NX Console

Hands-on

- 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

Hands-on

Create the NX Workspace

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

Hint

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

 Which CI provider would you like to use? · skip

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

Create the NX Workspace

Hands-on

Install the Nx Console plugin in VS Code

Hands-on

Setup & Exploration

Explore the file system

and identify the:

- apps/greenHeaven/project.json

- nx.json

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

Hands-on

Create the Home Feature using the NX Console

Hint

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

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),
  },
];

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"

Hands-on

Create the Routing

Ensure the Feature is Rendered

Nx Architecture &

The Intelligent Graph

Types, Scopes, and the Shell Pattern

In this lesson

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

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)

Feature Libraries

type:feature

The role

Smart components that orchestrate the UI components.

Contains

Rules

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

Page Layouts

Services / Facades

Domain-specific logic

GreenHeaven example

feature-catalogue

What it does

Orchestrates the entire page

UI Libraries

type:ui

The role

"Dumb" components focused on pure presentation.

Contains

GreenHeaven example

ui-product-card

Rules

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

What it does

A UI unit of the entire application

Cards

Business UI Components

Buttons

Data-Access Libraries

type:data-access

The role

Managing State and API communication.

Contains

HTTP Services

NGXS/NGRX

Angular Signals/State

GreenHeaven example

catalogue-data-access

What it does

Communicates with the HTTP API

Rules

It has no UI components

Utility Libraries

type:util

The role

Low-level, pure helper functions

Contains

What it does

Increases the reusability

Rules

Has zero dependencies on other library types.

Date formatters

Math helpers

Custom validators

GreenHeaven example

-

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

App

Util

Data

UI

Feat

UI

UI

Feat

Data

UI

UI

Util

UI

Util

Feat

Feat

Feat

Feat

Feat

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

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

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

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

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

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

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

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

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

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: '' },
];

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: '' },
];

App

Scope A

Scope B

Scope C

Feat

UI

Data

Util

UI

Feat

Util

Util

Feat

UI

Data

App

UI

Data

Util

Feat

Feat

UI

Util

Util

UI

Data

Feat

Scope A

Scope B

Scope C

Metadata and Tags

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

tag: segregates the technical responsibility 

scope: segregates the functional responsibility

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": { ... }
  }
}

Scope A

Util

Feat

UI

Data

Metadata and Tags

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

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",
	]
}

Metadata and Tags

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

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

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
// 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

Naming Convention

Naming consistency is key for tools and humans

Data

Util

UI

scope: users

Data

Util

UI

scope: catalogue

product-list

product-list

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

scope-type-identifier*

catalogue
shared
auth

feature
ui
data-access

catalogue-list
header
users*

Naming Convention

scope-type-identifier*

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

Naming Convention

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

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

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

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

The Intelligent Graph

Visualize if your architecture is messy > nx graph

The Intelligent Graph

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

The Intelligent Graph

Visualize if your architecture is messy > detect an error

Hands-on

- 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

Hands-on

Stash your changes

Preparation

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

Checkout

git checkout lab/one

Let me guide you through...

Hands-on

 Create a "shell" library for the Catalogue

Hint

- Update the app/app.routes.ts

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

- Use the NX console and give the correct project name

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

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

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

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

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

Hands-on

Identify and solve the problem with the product card

Hint

nx graph

Hands-on

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

Hint

nx graph

Introducing Signals

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

In this lesson

  • The Signals: What and Why
  • Signals API
  • Signals Graph
  • Signals Pull Push algorithm
  • LinkedSignal
  • rxResource and httpResource

- 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

- Traditional Change Detection can be slow on complex applications

Why Signals?

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

Zone.js

User Interaction

dom event (click)

zone.js

(Angular Zone)

Change Detection

UI Update

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

Default + Observable

OnPush + Observable

OnPush + Signals

OnPush + Observable

OnPush + Signals

OnPush + Signals

Observers

Subject

Consumers

Producers

Template

counter

Consumers

Producers

counter = signal<number>(0);

Producer

counter = signal<number>(0);

Returns a WritableSignal

Producer

counter = signal<number>(0);

Define the type

Producer

counter = signal<number>(0);

Default value

Producer

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

Consumer

Template Context

Consumer

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

Consumer & Producer

Producer

Consumer

computed

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

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

* source signal = mySignalArray()

Computed

effect

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

equality

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

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

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

- optionally provide an equality function

Equality

signal

inputs

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

read-only signal

export interface UserModel {
  name: string;
  age: number;
  
  /*Social*/
  address: string;
  twitter: string;
  linkedin: string;
  github: string;
  instagram: string;
  facebook: string;
  website: string;
  email: string;
}
@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);
    }
  }
}
@Component({...})
export class MyComponent {
  user = input.required<UserModel>();
  userSocials = computed(() => {
    const { name, age, ...userSocials } = this.user();
    return Object.values(userSocials);
  });
}

model input

@Component({...})
export class ChildComponent {
  @Input({ required: true }) name!: string;
  @Output() nameChange = new EventEmitter<string>();
}
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child 	[name]="username" 
				(nameChange)="changeHandler($event)" />
  `,
})
export class ParentComponent { 
   username = 'profanis';
}
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child 	[name]="username" 
				(nameChange)="changeHandler($event)" />
  `,
})
export class ParentComponent { 
   username = 'profanis';
}
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: ` <app-child [(name)]="username" /> `,
})
export class ParentComponent {
  username = 'profanis';
}
@Component({...})
export class ChildComponent {
  // @Input({ required: true }) name!: string;
  // @Output() nameChange = new EventEmitter<string>();
  name = model<string>();
}
@Component({...})
export class ChildComponent {
  // @Input({ required: true }) name!: string;
  // @Output() nameChange = new EventEmitter<string>();
  name = model.required<string>();
}
@Component({...})
export class ChildComponent {
  name = model<string>(); // writable signal

  addTitle() {
    this.name.update((name) => `Mr. ${name}`);
  }
}
@Component({...})
export class ChildComponent {
  name = model<string>(); // writable signal

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

new output

@Component({...})
export class ChildComponent {
  name = input.required<string>();
  @Output() nameChange = new EventEmitter<string>();
}
@Component({...})
export class ChildComponent {
  name = input.required<string>();
  nameChange = output<string>()
}
@Component({...})
export class ChildComponent {
  @Output() formIsValid = this.form.statusChanges.pipe(
     map((status) => status === 'VALID'),
  );
}
import { outputFromObservable } from '@angular/core/rxjs-interop';

@Component({...})
export class ChildComponent {
  formIsValid = outputFromObservable(
    this.form.statusChanges.pipe(
            map((status) => status === 'VALID'))
  );
}
import { outputToObservable } from '@angular/core/rxjs-interop';

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

Hands-on

- Stop calling a function in the template. Use computed

- Create a filter component and use signal input/output

- Manage Favorite Products

Hands-on

Stash your changes

Preparation

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

Checkout

git checkout lab/two

Let me guide you through...

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
 

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


4. Update the template

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

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

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

Advanced Reactivity

Resources, I/O & Forms

In this lesson

  • The Signals: What and Why
  • Signals API
  • Signals Graph
  • Signals Pull Push algorithm
  • LinkedSignal
  • rxResource and httpResource

data fetching

signal<string>

signal<string>

signal<string>

cancel

cancel

How about effect?

time - 0

time - 1

request

request

100ms

500ms

time - 0

time - 1

request

request

100ms

500ms

time - 0

time - 1

request

request

100ms

500ms

time - 0

time - 1

request

request

100ms

500ms

Sync

Async

At some point we will have the data

At some point we will have the data

We will always have data

http

isLoading

error

data

http

isLoading()

error()

data()

httpResource

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

httpResource(
	?, 
	?
)
httpResource(
	string | object | function , 
	?
)
httpResource(
	string | object | function , 
	options
)
// String
httpResource(`https://api.com/${signalValue()}`)

dependency

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

dependency

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

dependency

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

Http verb

// Function
httpResource(() => `https://api.com/${signalValue()}`)
// Function
httpResource(() => 
             signalValue() ? 
             `https://api.com/${signalValue()}` : 
             undefined
)
// String with Options
httpResource(`https://api.com/${signalValue()}`, {
	defaultValue: {},
	parse: (response) => zodSchema.parse(response),
})
// 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>
}
// String
resource = httpResource(`https://api.com/${signalValue()}`)
derivedState = computed(
  () => resource.value().map(it => it.title)
)
resource = rxResource({
  params: () => ({
    paramName: this.paramNameAsSignal(),
  }),
  stream: ({ params }) => this.serviceApi.get(params.paramName),
});

Dependency

Service to call

- explain the reload

- explain the debounce

- explain the rxResource

 

linkedSignal

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)
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)
listOfItems = signal(['item1', 'item2', 'item3']);

countOfItems = linkedSignal({
    source: this.listOfItems,
    computation: (items) => items.length, // 3
});
countOfItems.set(0)
listOfItems = signal(['item1', 'item2', 'item3', 'item4']);

countOfItems = linkedSignal({
    source: this.listOfItems,
    computation: (items) => items.length, // 4
});
countOfItems.set(0)
listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);


// keeps the selected item
selectedItem = listOfItems[0];
listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);



// an HTTP call is happening
http.get().subscribe(data => 
	this.listOfItems.set([...data])
)
listOfItems = signal([ 
  { name: 'item 1' }, 
  { name: 'item 2' }, 
  { name: 'item 3' } 
]);





selectedItem = signal<Item | null>(null);
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),
});

The Graph

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

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

Pull - Push

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

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

// Update signal values
isValid.set(false);
username.set('profanis2');
effect(() => {
  if (isValid() === true) {
    console.log(username());
  }
});

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

Consumers

Producers

isValid

effect

username

Push (notification)

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

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

Consumers

Producers

isValid

username

Push (notification)

effect

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

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

Consumers

Producers

isValid

username

Pull (value)

effect

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

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

Consumers

Producers

isValid

username

Push (notification)

effect

Consumers

Producers

isValid

username

Push (notification)

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

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

effect

Consumers

Producers

isValid

username

Pull (value)

Pull (value)

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

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

effect

effect

isValid

username

Hands-on

- Fetch data using the rxResource

- Filter data in a computed function

- Load more products using a LinkedSignal

Hands-on

Stash your changes

Preparation

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

Checkout

git checkout lab/one

Let me guide you through...

Hands-on

Fetch data using the rxResource

- Fetch data using rxResource

- Apply a pagination

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...

Hands-on

Fetch data using rxResource

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

Hands-on

Apply a pagination

- In the catalogue.component.html invoke the onLoadMore

- Use a LinkedSignal

State Management in a Functional Way

By Fanis Prodromou

State Management in a Functional Way

Angular Signals: A Look Under the Hood and Beyond

  • 14