Angular
Pathrule4 Rules • 2 Memories
Modern Angular looks almost nothing like the Angular most training data remembers. On the current line, standalone components are the default and NgModule is gone from new code, signals are the primary reactive primitive, and zoneless change detection is the default for new apps rather than an experiment. This bundle keeps an assistant on that surface: signals and computed for state, the built-in control flow blocks with a required track, inject() and functional interceptors, and resource-based data loading instead of hand-rolled subscription bookkeeping.
Suggested path map
Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.
Rules
4Hold component state in signals, not in fields updated by subscribe/src/apphighstrictUse signal, computed, and linkedSignal for state, derive instead of recompute, and convert streams with toSignal rather than subscribing by hand.
| 1 | Signals are Angular's reactive primitive now, and they are what makes zoneless change detection precise. A component that stores state in plain fields and updates them from a subscription is invisible to that machinery. |
| 2 | |
| 3 | - Declare mutable state with `signal()`, derived state with `computed()`, and inputs with the `input()` / `model()` signal APIs. Never recompute a derived value in a lifecycle hook. |
| 4 | - Convert an observable at the edge with `toSignal()` instead of subscribing in the component. If you must subscribe (a side effect, not a value), pipe through `takeUntilDestroyed()` so teardown is automatic. |
| 5 | - Do not write to a signal inside a `computed()`. Use `effect()` only for genuine side effects (analytics, DOM APIs, storage), never to sync one signal into another; that is what `computed` and `linkedSignal` are for. |
| 6 | - Read signals as function calls in the template (`count()`), and keep the reads shallow: a template that calls a method which loops is a re-render cost on every check. |
| 7 | - Keep RxJS for what it is genuinely good at (event streams, debouncing, cancellation) and let signals hold the state those streams produce. |
Standalone components only, with lazy routes/src/apphighstrictNo new NgModule: components, directives, and pipes are standalone, dependencies are declared in imports, and routes lazy-load components.
| 1 | Standalone is the default in current Angular, and mixed-mode projects pay the cost of both worlds. New code declares its own dependencies. |
| 2 | |
| 3 | - Write components, directives, and pipes as standalone and list what a template uses in that component's `imports`. Do not create an NgModule for new code, and do not import a barrel module that pulls in half the app. |
| 4 | - Bootstrap with `bootstrapApplication` and configure the app through `ApplicationConfig` providers (`provideRouter`, `provideHttpClient`, `provideAnimationsAsync`), not through module metadata. |
| 5 | - Lazy-load at the route with `loadComponent` (or `loadChildren` pointing at a route array). A route that imports its component eagerly puts it in the initial bundle. |
| 6 | - Prefer `providedIn: 'root'` services so they tree-shake, and scope a provider to a route or component only when you genuinely need a separate instance. |
| 7 | - When migrating, move leaves first: convert a component to standalone, delete it from the module's declarations, and repeat. `ng generate @angular/core:standalone` does most of the mechanical work. |
Write zoneless-safe code/src/apphighstrictNever rely on Zone.js to notice a change: state changes go through signals, and manual detectChanges or markForCheck calls are a smell.
| 1 | New Angular apps run without Zone.js, so the framework no longer patches timers, promises, and event handlers to guess when something changed. Code that assumed it did will render stale UI. |
| 2 | |
| 3 | - Route every state change through a signal (or an explicitly `markForCheck`-ed OnPush component). Mutating a plain field inside a `setTimeout`, a promise callback, or a third-party library callback updates nothing on screen in a zoneless app. |
| 4 | - Assume `OnPush` semantics everywhere and stop reaching for `ChangeDetectorRef.detectChanges()` to force a paint. If a value needs to be visible, it needs to be a signal. |
| 5 | - Do not depend on `NgZone.onMicrotaskEmpty`, `whenStable`, or zone-based stability for logic. For tests, use `await fixture.whenStable()` with the modern harness rather than `fakeAsync` tick tricks. |
| 6 | - Third-party callbacks (charts, maps, web components, WebSocket clients) are the usual breakage point: set a signal inside the callback and let the framework do the rest. |
| 7 | - Keep it enabled deliberately: `provideZonelessChangeDetection()` on versions where it is opt-in, and no `zone.js` import creeping back into polyfills. |
Use the built-in control flow with a real track expression/src/appmediumstrictTemplates use the block syntax, every loop tracks a stable identity, and templates never call functions that compute.
| 1 | The block control flow (`@if`, `@for`, `@switch`, `@defer`) is the current template surface. It is not sugar over the old directives: it ships less runtime and behaves better under signal-driven rendering. |
| 2 | |
| 3 | - Use `@if` / `@else if`, `@for` with a mandatory `track`, and `@switch`. Do not add `NgIf`, `NgFor`, or `NgSwitch` to a new component's imports. |
| 4 | - `track` must be a stable identity (`track item.id`), not `$index`, unless the list is genuinely positional. Tracking by index re-creates DOM and destroys focus and animation state when the list reorders. |
| 5 | - Use `@empty` for the empty state rather than a second `@if` over the same collection. |
| 6 | - Defer expensive, below-the-fold subtrees with `@defer (on viewport)` and give them a `@placeholder`. This is the cheapest real win on initial load. |
| 7 | - Never call a function or getter that filters, sorts, or maps in a template. Compute it once in a `computed()`; a template expression can run on every change detection pass. |
Memories
2Injection and the functional APIs that replaced classes/src/app/coreinject() in field initializers, functional guards, resolvers, and interceptors, and DI tokens for configuration instead of ambient imports.
| 1 | Angular's DI surface moved from class-based to functional, and the class-based variants are either deprecated or legacy. New code should not reintroduce them. |
| 2 | |
| 3 | - Use `inject()` in a field initializer instead of constructor parameters. It composes into helper functions, works in functional guards and interceptors, and keeps subclass constructors free of super-call plumbing. |
| 4 | - Guards and resolvers are plain functions (`CanActivateFn`, `ResolveFn`). The class-based `CanActivate` interface is deprecated: a guard is a function that injects what it needs and returns a boolean, a redirect, or an observable of one. |
| 5 | - HTTP interceptors are functions registered with `provideHttpClient(withInterceptors([...]))`. Use them for auth headers, correlation ids, and retry policy, and keep them small and ordered deliberately. |
| 6 | - Pass configuration through an `InjectionToken` provided at bootstrap rather than importing a config object. It keeps the app testable and lets an environment override one value without a rebuild. |
| 7 | - Prefer `providedIn: 'root'` for stateless services. A service that holds state should hold it in signals so consumers get change detection for free. |
| 8 | |
| 9 | See /src/app for the signal and zoneless rules these APIs assume. |
Loading data with resources instead of subscription bookkeeping/src/app/coreSignal-based resource APIs give you value, status, and error in one place, with request cancellation, so components stop tracking loading flags by hand.
| 1 | Most Angular data code is the same three fields written by hand: value, loading, error. The signal-based resource APIs replace that, and they cancel in-flight requests when their inputs change. |
| 2 | |
| 3 | - Fetch with `httpResource()` for a plain HTTP read, or `resource()` when the loader wraps something else (an SDK, several calls, a reshaped payload). Both expose signals for the value and the request status, so the template reads them directly. |
| 4 | - Make the request reactive by deriving it from signals: when the id signal changes, the resource reloads and the previous request is cancelled. That is the part hand-rolled subscriptions almost always get wrong. |
| 5 | - Render status from the resource itself rather than a separate `isLoading` signal you keep in sync. Two sources of truth for one request is how spinners get stuck. |
| 6 | - Keep mutations explicit: a POST or PATCH is an action that calls `HttpClient` and then reloads or updates the resource. Do not model a write as a resource. |
| 7 | - For anything that needs a cache across components, put the resource in a `providedIn: 'root'` service and expose read-only signals. Reach for a full state library only when you have real cross-cutting state, not to store one list. |
| 8 | |
| 9 | See /src/app for the state rule; the tanstack-query pattern covers the same problem for React if you share a monorepo. |
Why this pattern
AI agents write Angular the way it looked five years ago: NgModule declarations, constructor injection, manual subscribe calls with no teardown, and ngIf and ngFor in templates.
Built for Frontend teams on the current Angular line, migrating off NgModule and RxJS-only state.
Keeps your assistant from:
- Generating NgModule boilerplate for components that should be standalone
- Calling subscribe in a component with no teardown, leaking on every navigation
- Assuming Zone.js will notice a change, which never happens in a zoneless app
- Rendering a list with no track expression and re-creating every DOM node
- License
- Apache-2.0
- Version
- 1.0.0
- Updated
- 2026-08-24