Tools: What is "effect" in Angular(v21)?

Tools: What is "effect" in Angular(v21)?

Source: Dev.to

1. What is an Effect? ## 2. Basic Syntax ## 3. What can you do with Effects? ## 4. The Rules of Effects ## 5. Cleaning Up (The onCleanup function) While Writable Signals hold data and Computed Signals transform data, an Effect is the "performer". An Effect is a function that runs automatically whenever the signals it uses change. Unlike the other two types of signals, an effect doesn't return a value. Instead, it performs a task. Think of it as a loyal assistant watching your signals. The moment a signal changes, the assistant jumps up and executes the specific job you gave them—like saving a file, updating a chart, or logging a message. You define an effect using the effect() function, usually inside a component's constructor. Effects are designed for "side effects"—actions that need to happen because data changed, but aren't part of the UI rendering itself. Can use async functions as well Effects trigger only once after all the dependent signals are updated(even when updated in multiple places in the code). It will trigger once when the thread is ideal and empty. Effects are asynchronous and batched When you update a signal, the effect is "scheduled" to run. It doesn't jump the line and execute immediately. Instead, it waits for the current Microtask to finish. Think of it like a waiter at a restaurant: if three people at the table order water at different times during the conversation, the waiter doesn't run to the kitchen three times. They wait for you to finish talking, take the whole order, and then make one trip. Angular batches updates naturally during any code execution (like an event handler or an HTTP response). Common use cases include: Logging/Analytics: Sending data to a server when a user reaches a certain milestone. External APIs: Manually manipulating a non-Angular library (like a D3.js chart or a Google Map). LocalStorage: Saving the state of a form so the user doesn't lose progress if they refresh. Custom Notifications: Triggering a "toast" message or a sound effect. To keep your app stable, Angular enforces a few "safety rails" for effects: No Signal Writes (By Default) You cannot update a signal inside an effect. Why? If Signal A triggers an Effect that updates Signal A, you create an infinite loop that crashes your browser. Exception: If you absolutely must, you can enable allowSignalWrites: true, but it's generally a sign that you should be using a computed signal instead. Injection Context Effects must be created where Angular has access to "injection context" (usually the constructor or as a class member). If you try to create an effect inside a random method like onClick(), it will fail unless you manually pass it a reference to the app's injector. Sometimes an effect starts a task that needs to be stopped before the next run (like a timer or a network request). Effects give you a onCleanup function for this: Think of effect as your bridge to the outside world. It’s the designated spot for "side effects"—like logging or API syncing—that should run automatically and efficiently in the background whenever your state changes. Thanks for reading. Happy coding!!! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: import { Component, signal, effect } from '@angular/core'; @Component({...}) export class UserProfile { username = signal('Harry'); constructor() { // This effect runs immediately once, then every time 'username' changes effect(() => { console.log(`The username is now: ${this.username()}`); }); } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { Component, signal, effect } from '@angular/core'; @Component({...}) export class UserProfile { username = signal('Harry'); constructor() { // This effect runs immediately once, then every time 'username' changes effect(() => { console.log(`The username is now: ${this.username()}`); }); } } COMMAND_BLOCK: import { Component, signal, effect } from '@angular/core'; @Component({...}) export class UserProfile { username = signal('Harry'); constructor() { // This effect runs immediately once, then every time 'username' changes effect(() => { console.log(`The username is now: ${this.username()}`); }); } } COMMAND_BLOCK: // async code effect( async () => await service.set(x) ) // side effects effect(() => storage.save() ) Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // async code effect( async () => await service.set(x) ) // side effects effect(() => storage.save() ) COMMAND_BLOCK: // async code effect( async () => await service.set(x) ) // side effects effect(() => storage.save() ) COMMAND_BLOCK: count = signal(0); constructor() { effect(() => { console.log('Effect ran with:', this.count()); }); } updateMultipleTimes() { this.count.set(1); this.count.set(2); this.count.set(3); // The console will NOT show 1 and 2. // It will wait until this function is DONE, then log "3". } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: count = signal(0); constructor() { effect(() => { console.log('Effect ran with:', this.count()); }); } updateMultipleTimes() { this.count.set(1); this.count.set(2); this.count.set(3); // The console will NOT show 1 and 2. // It will wait until this function is DONE, then log "3". } COMMAND_BLOCK: count = signal(0); constructor() { effect(() => { console.log('Effect ran with:', this.count()); }); } updateMultipleTimes() { this.count.set(1); this.count.set(2); this.count.set(3); // The console will NOT show 1 and 2. // It will wait until this function is DONE, then log "3". } CODE_BLOCK: // Even if you update two completely different signals updateBoth() { this.firstName.set('Jane'); this.lastName.set('Doe'); } // An effect listening to both // will only trigger ONCE after updateBoth() finishes. Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Even if you update two completely different signals updateBoth() { this.firstName.set('Jane'); this.lastName.set('Doe'); } // An effect listening to both // will only trigger ONCE after updateBoth() finishes. CODE_BLOCK: // Even if you update two completely different signals updateBoth() { this.firstName.set('Jane'); this.lastName.set('Doe'); } // An effect listening to both // will only trigger ONCE after updateBoth() finishes. COMMAND_BLOCK: effect((onCleanup) => { const timer = setInterval(() => { console.log('Ping!'); }, 1000); // This runs right before the effect re-runs or the component is destroyed onCleanup(() => { clearInterval(timer); }); }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: effect((onCleanup) => { const timer = setInterval(() => { console.log('Ping!'); }, 1000); // This runs right before the effect re-runs or the component is destroyed onCleanup(() => { clearInterval(timer); }); }); COMMAND_BLOCK: effect((onCleanup) => { const timer = setInterval(() => { console.log('Ping!'); }, 1000); // This runs right before the effect re-runs or the component is destroyed onCleanup(() => { clearInterval(timer); }); }); - Logging/Analytics: Sending data to a server when a user reaches a certain milestone. - External APIs: Manually manipulating a non-Angular library (like a D3.js chart or a Google Map). - LocalStorage: Saving the state of a form so the user doesn't lose progress if they refresh. - Custom Notifications: Triggering a "toast" message or a sound effect. - Why? If Signal A triggers an Effect that updates Signal A, you create an infinite loop that crashes your browser. - Exception: If you absolutely must, you can enable allowSignalWrites: true, but it's generally a sign that you should be using a computed signal instead.