How does effect() work in Angular signals?
`effect()` is a fundamental primitive in Angular signals designed to run side effects in response to signal value changes. Unlike `computed()`, which derives new signal values reactively, `effect()` does not produce a value itself; instead, it's used for operations that interact with the outside world or perform non-reactive computations.
Core Functionality of effect()
An effect() creates a reactive context where code will automatically re-execute whenever any signal it reads changes. It's ideal for tasks that need to run *because* a signal changed, but don't result in another signal being updated directly within the effect's scope (unless explicitly configured to do so). Effects always run at least once immediately after they are created.
Key Characteristics and Behavior
- Side Effects Only: Primarily intended for side-effectful operations like logging, manually updating the DOM, interacting with browser APIs, or synchronizing with non-Angular code.
- Non-reactive Output: An
effect()doesn't return a value that can be used elsewhere in your application. Its purpose is to *do* something, not to *provide* something. - Automatic Re-execution: The function passed to
effect()will automatically re-run whenever any of the signals it accesses during its execution change their values. - Immediate Execution: An effect runs once immediately after it's created, allowing it to establish its initial state or perform an initial action.
- Cleanup Function: The function passed to
effect()can optionally return another function. This returned function acts as a cleanup mechanism, which will be executed before the effect re-runs or when the effect is destroyed. - Injection Context:
effect()requires an injection context. Typically, it's called within a component, directive, service, or standalone function where Angular's DI system is active. - Lifecycle Management: Effects created in a component or service are automatically destroyed when that component or service is destroyed, preventing memory leaks.
- Allow Signal Writes (Advanced): By default, an effect cannot write to other signals to prevent infinite loops. However, this can be overridden with the
{ allowSignalWrites: true }option, though it should be used with extreme caution and only when strictly necessary, as it can lead to complex and hard-to-debug reactivity cycles.
Basic Syntax
import { signal, effect } from '@angular/core';
@Component({ /* ... */ })
export class MyComponent {
count = signal(0);
name = signal('World');
constructor() {
effect(() => {
console.log(`The count is: ${this.count()}, Name: ${this.name()}`);
});
effect((onCleanup) => {
console.log('Effect with cleanup: Initial run');
const intervalId = setInterval(() => {
console.log(`Running every 1s, count is: ${this.count()}`);
}, 1000);
// This cleanup function runs before the effect re-runs or on destruction
onCleanup(() => {
console.log('Cleaning up interval');
clearInterval(intervalId);
});
});
}
increment() {
this.count.update(value => value + 1);
}
changeName() {
this.name.set('Angular');
}
}
In the example above, the first effect will log a message to the console every time this.count or this.name changes. The second effect demonstrates a cleanup function, ensuring that the interval is cleared when the effect's dependencies change or when the component is destroyed.
When to Use effect()
- Logging: Debugging and understanding state changes.
- Synchronizing with non-Angular code: Integrating with third-party JavaScript libraries (e.g., charting libraries, map APIs) that require direct DOM manipulation or explicit updates based on application state.
- Manually updating the DOM: When you need to interact directly with the DOM outside of Angular's rendering pipeline (use sparingly, prefer templates when possible).
- Browser API interactions: Such as updating the document title, managing local storage, or subscribing to browser events.
- Triggering network requests: For example, fetching data from an API whenever a filter signal changes (though be mindful of request frequency and cancellation).
Important Considerations
- Keep it focused: Effects should ideally be small, focused on a single side effect, and avoid complex logic.
- Avoid heavy computation: If you need to derive new state, use
computed()instead, aseffect()is not optimized for value production. - Prefer
computed()for derived state: If the result of your reactive computation is another value that can be consumed by other parts of your application,computed()is almost always the better choice. - Beware of infinite loops: If an effect writes to a signal that it also reads (without
allowSignalWrites: true), it will cause an infinite loop. Even withallowSignalWrites: true, ensure your logic prevents such loops.