Lifecycle Visual Diagrams
Visual explanations of Aurelia 2's component lifecycle with parent-child timing.
Table of Contents
1. Activation Sequence (Parent-Child)
How lifecycle hooks execute when activating a parent with children:
SCENARIO: Parent component with 2 children activates
═══════════════════════════════════════════════════════════════
Timeline:
─────────────────────────────────────────────────────────────
Time Parent Child-1 Child-2
──── ────── ─────── ───────
0 constructor()
1 define()
2 hydrating()
3 hydrated()
created() ←──────── created() ←─────── created()
│ │
└─ children first ─┘
4 binding() ──────┐
↓ (if async, │
blocks children)│
│
5 ← resolve ──────┘
bind() (connects bindings)
6 attaching() ────┐
_attach() DOM ──┤ binding() ────┐ binding() ────┐
│ │ │
│ bind() │ bind() │
│ │ │
│ attaching() ──┤ attaching() ───┤
│ _attach() DOM │ _attach() DOM │
│ │ │
┌───┴────────────────┴──────────────────┘
│ (parent's attaching() and
│ children activation run in PARALLEL)
│
7 └─→ Wait for all to complete
attached() ←───── attached() ←──── attached()
│ │ │
└─ children first (bottom-up) ─────┘
8 ACTIVATED
DETAILED ACTIVATION FLOW
═══════════════════════════════════════════════════════════
┌────────────────────────────────────────────────────┐
│ CONSTRUCTION PHASE (Top ➞ Down) │
├────────────────────────────────────────────────────┤
│ │
│ Parent.constructor() │
│ → Child1.constructor() │
│ → Child2.constructor() │
│ │
│ Parent.define() │
│ → Child1.define() │
│ → Child2.define() │
│ │
│ Parent.hydrating() │
│ → Child1.hydrating() │
│ → Child2.hydrating() │
│ │
│ Parent.hydrated() │
│ → Child1.hydrated() │
│ → Child2.hydrated() │
│ │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ CREATED PHASE (Bottom ➞ Up) │
├────────────────────────────────────────────────────┤
│ │
│ Child1.created() │
│ Child2.created() │
│ → Parent.created() ← After all children │
│ │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ BINDING PHASE (Top ➞ Down, Blocks Children) │
├────────────────────────────────────────────────────┤
│ │
│ Parent.binding() │
│ ↓ (if async, children wait) │
│ ↓ │
│ [ await parent.binding() ] │
│ ↓ │
│ Parent.bind() - connects bindings to scope │
│ ↓ │
│ Child1.binding() │
│ ↓ │
│ [ await child1.binding() ] │
│ ↓ │
│ Child1.bind() │
│ │
│ Child2.binding() │
│ ↓ │
│ [ await child2.binding() ] │
│ ↓ │
│ Child2.bind() │
│ │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ BOUND PHASE (Bottom ➞ Up) │
├────────────────────────────────────────────────────┤
│ │
│ Child1.bound() │
│ Child2.bound() │
│ → Parent.bound() ← After children │
│ │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ ATTACHING PHASE (Parallel!) │
├────────────────────────────────────────────────────┤
│ │
│ Parent.attaching() activatingStack = 1 │
│ ↓ ↓ │
│ Parent._attach() │ │
│ → Append to DOM │ │
│ │ │
│ [ Both run in PARALLEL ] │ │
│ ├─ await parent.attaching() │ │
│ └─ Child activation ───────────┘ │
│ ├─ Child1.binding() activatingStack++ │
│ ├─ Child1.bind() │
│ ├─ Child1.bound() │
│ ├─ Child1.attaching() │
│ ├─ Child1._attach() │
│ │ → Append to DOM │
│ │ │
│ ├─ Child2.binding() activatingStack++ │
│ ├─ Child2.bind() │
│ ├─ Child2.bound() │
│ ├─ Child2.attaching() │
│ └─ Child2._attach() │
│ → Append to DOM │
│ │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ ATTACHED PHASE (Bottom ➞ Up) │
├────────────────────────────────────────────────────┤
│ │
│ _leaveActivating() called on each child │
│ activatingStack-- for each │
│ │
│ When activatingStack === 0: │
│ Child1.attached() activatingStack-- │
│ Child2.attached() activatingStack-- │
│ → Parent.attached() activatingStack-- │
│ ↓ │
│ activatingStack === 0 │
│ → state = activated │
│ │
└────────────────────────────────────────────────────┘
KEY IMPLEMENTATION DETAILS
═══════════════════════════════════════════════════════════
1. _enterActivating() increments activatingStack
- Called when starting binding phase
- Recursively increments parent's stack
2. Parent's attaching() runs in PARALLEL with children
- Children start activating while parent is still attaching
- This allows for better performance
3. attached() only called when stack === 0
- _leaveActivating() decrements stack
- When stack reaches 0, attached() is invoked
- This ensures bottom-up execution
4. binding() can block
- If it returns a Promise, children wait
- This is why it's marked "blocks children" in docs2. Deactivation Sequence (Parent-Child)
How lifecycle hooks execute when deactivating:
SCENARIO: Parent with 2 children deactivates
═══════════════════════════════════════════════════════════
Timeline:
─────────────────────────────────────────────────────────────
Time Parent Child-1 Child-2
──── ────── ─────── ───────
0 deactivate() ───┐
│
1 └──→ deactivate() ──→ deactivate()
│ │
(children first) │
│ │
2 detaching() ←─────┘
│
(builds linked list)
│
3 detaching() ←────────┘
│
(initiator collects all)
│
4 _leaveDetaching()
└─→ detachingStack === 0
5 removeNodes() ──┐
├─→ removeNodes()
└─→ removeNodes()
(DOM removed from all)
6 unbinding() ────┐
├─→ unbinding()
└─→ unbinding()
(process linked list)
7 unbind() ───────┐
├─→ unbind()
└─→ unbind()
DEACTIVATED
DETAILED DEACTIVATION FLOW
═══════════════════════════════════════════════════════════
┌────────────────────────────────────────────────────┐
│ CHILD DEACTIVATION (Children First) │
├────────────────────────────────────────────────────┤
│ │
│ Parent.deactivate() called │
│ ↓ │
│ state = deactivating │
│ ↓ │
│ for each child: │
│ child.deactivate(initiator, parent) │
│ ↓ │
│ Child1.deactivate() ────┐ │
│ Child2.deactivate() ────┤ │
│ │ │
└───────────────────────────────┼────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ DETACHING PHASE (Children First, Build List) │
├────────────────────────────────────────────────────┤
│ │
│ Child1.detaching() detachingStack++ │
│ ↓ (await if async) │
│ Add Child1 to linked list │
│ │
│ Child2.detaching() detachingStack++ │
│ ↓ (await if async) │
│ Add Child2 to linked list │
│ │
│ Parent.detaching() detachingStack++ │
│ ↓ (await if async) │
│ Add Parent to linked list │
│ │
│ Linked list: Child1 → Child2 → Parent │
│ │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ DETACH PHASE (Initiator Processes All) │
├────────────────────────────────────────────────────┤
│ │
│ Initiator._leaveDetaching() │
│ ↓ │
│ detachingStack-- │
│ ↓ │
│ When stack === 0: │
│ Process linked list: │
│ ↓ │
│ Parent.removeNodes() │
│ Child1.removeNodes() │
│ Child2.removeNodes() │
│ ↓ │
│ (DOM physically removed) │
│ │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ UNBINDING PHASE (Process List, Children First) │
├────────────────────────────────────────────────────┤
│ │
│ Walk linked list (Child1 → Child2 → Parent): │
│ │
│ Child1.unbinding() unbindingStack++ │
│ ↓ (await if async) │
│ Child1.unbind() │
│ → disconnect bindings │
│ → scope = null │
│ │
│ Child2.unbinding() unbindingStack++ │
│ ↓ (await if async) │
│ Child2.unbind() │
│ → disconnect bindings │
│ → scope = null │
│ │
│ Parent.unbinding() unbindingStack++ │
│ ↓ (await if async) │
│ Parent.unbind() │
│ → disconnect bindings │
│ → scope.parent = null │
│ → state = deactivated │
│ │
└────────────────────────────────────────────────────┘
KEY IMPLEMENTATION DETAILS
═══════════════════════════════════════════════════════════
1. Children deactivate first
- Parent calls deactivate on each child
- Children process before parent continues
2. Linked list built during detaching
- Each component adds itself to the list
- List maintains deactivation order
3. Only initiator processes the list
- Non-initiator components just add themselves
- Initiator handles all DOM removal and unbinding
4. removeNodes() called before unbinding
- DOM physically removed first
- Then bindings are disconnected
5. unbinding() processed via linked list
- Walks the list in order
- Calls unbinding hooks sequentially
PARALLEL DETACHING
═══════════════════════════════════════════════════════════
When detaching() returns a Promise:
Parent.detaching() ─────┐
├─ await (parallel)
Child1.detaching() ─────┤
├─ await (parallel)
Child2.detaching() ─────┘
All detaching() hooks await in PARALLEL, then:
→ removeNodes() on all
→ unbinding() in sequence via linked list
→ unbind() completes deactivation
This allows exit animations to run simultaneously!3. Stack-Based Coordination
How Aurelia ensures correct timing using activation/deactivation stacks:
ACTIVATION STACK MECHANISM
═══════════════════════════════════════════════════════════
Purpose: Ensure attached() only fires after ALL children are ready
private _activatingStack: number = 0;
_enterActivating() {
++this._activatingStack; // Increment
if (this.$initiator !== this) {
this.parent._enterActivating(); // Propagate up
}
}
_leaveActivating() {
if (--this._activatingStack === 0) { // Decrement
// Stack is 0, all children done!
this.attached(); // Call attached()
this.state = activated;
}
if (this.$initiator !== this) {
this.parent._leaveActivating(); // Propagate up
}
}
STACK TIMELINE (Parent + 2 Children)
═══════════════════════════════════════════════════════════
Time Action Parent Stack
──── ────── ────────────
0 Parent.activate() 0
1 _enterActivating() (binding) 1 (Enter)
2 Parent.binding() completes 1
3 Parent.bind() 1
4 Parent.attaching() 1
5 _enterActivating() (attaching) 2 (Enter again)
6 Child1 starts activating 3 (Child enters)
7 Child1 attaching 3
8 Child2 starts activating 4 (Child enters)
9 Child2 attaching 4
10 Parent.attaching() completes 4
11 _leaveActivating() (attaching) 3 (Leave)
12 Child1.attaching() completes 3
13 Child1 _leaveActivating() 2 (Child leaves)
14 Child1.attached() 2 (Stack > 0, can't call parent yet)
15 Child2.attaching() completes 2
16 Child2 _leaveActivating() 1 (Child leaves)
17 Child2.attached() 1
18 _leaveActivating() (binding) 0 (Stack === 0!)
19 Parent.attached() ----------------- 0 (NOW parent can fire)
state = activated
DETACHING STACK MECHANISM
═══════════════════════════════════════════════════════════
Purpose: Await all detaching() Promises before removing DOM
private _detachingStack: number = 0;
_enterDetaching() {
++this._detachingStack;
}
_leaveDetaching() {
if (--this._detachingStack === 0) {
// All detaching() complete!
this.removeNodes(); // Now safe to remove DOM
// Process unbinding via linked list...
}
}
DETACHING STACK TIMELINE
═══════════════════════════════════════════════════════════
Time Action Stack
──── ────── ─────
0 Parent.deactivate() 0
1 Child1.deactivate() 0
2 Child1.detaching() -> Promise 0
3 _enterDetaching() 1 (Track Promise)
4 Child2.deactivate() 1
5 Child2.detaching() -> Promise 1
6 _enterDetaching() 2 (Track Promise)
7 Parent.detaching() -> Promise 2
8 _enterDetaching() 3 (Track Promise)
9 Child1 Promise resolves 3
10 _leaveDetaching() 2 (Done)
11 Child2 Promise resolves 2
12 _leaveDetaching() 1 (Done)
13 Parent Promise resolves 1
14 _leaveDetaching() 0 (Stack === 0!)
removeNodes() on all -------------- 0 (Now safe)
unbinding() on all ---------------- 0
WHY STACKS ARE NECESSARY
═══════════════════════════════════════════════════════════
Problem without stacks:
- Parent's attached() might fire before children ready
- DOM might be removed while animations still running
- Race conditions between parent and children
Solution with stacks:
- attached() only fires when stack === 0 (all done)
- DOM only removed when all detaching() complete
- Clean coordination between parent and children
MULTIPLE ENTER/LEAVE CALLS
═══════════════════════════════════════════════════════════
A single controller can call _enterActivating() multiple times:
1. Once for binding phase
2. Once for attaching phase
This is intentional! The stack tracks ALL pending work:
- Parent's own lifecycle phases
- Each child's activation
When stack reaches 0, everything is truly done.4. Async Lifecycle Behavior
How async hooks affect timing:
SYNC VS ASYNC BINDING
═══════════════════════════════════════════════════════════
Synchronous binding():
──────────────────────────────────────────
export class MyComponent {
binding() {
this.data = setupData(); // ← Sync
}
}
Timeline:
Parent.binding() ──┐
├─ immediate
Parent.bind() ─────┘
↓
Child.binding() ───┐
├─ immediate
Child.bind() ──────┘
Total: ~0ms blocking time
Asynchronous binding():
──────────────────────────────────────────
export class MyComponent {
async binding() {
this.data = await fetch('/api/data'); // ← Async
}
}
Timeline:
Parent.binding() ──┐
├─ await ─────────────┐ (500ms)
│ │
│ (children blocked) │
│ │
└─────────────────────┘
Parent.bind() ─────┘
↓
Child.binding() ───┐ ← Only starts after parent resolves
├─ immediate
Child.bind() ──────┘
Total: ~500ms blocking time
REAL-WORLD IMPACT
═══════════════════════════════════════════════════════════
Bad - Blocks children unnecessarily:
export class Parent {
async binding() {
// This blocks children for 1 second!
await delay(1000);
this.data = 'loaded';
}
}
Good - Use loading() instead:
export class Parent {
async loading() {
// Children can start while this runs
await delay(1000);
this.data = 'loaded';
}
binding() {
// Sync, doesn't block children
}
}
ATTACHING() DOESN'T BLOCK CHILDREN
═══════════════════════════════════════════════════════════
Key difference from binding():
export class Parent {
async attaching() {
// This runs in PARALLEL with children!
await animateIn();
}
}
Timeline:
Parent.attaching() ────┐
├─ async animation (parallel)
Child activation ──────┤
├─ runs simultaneously
Both complete ─────────┘
↓
Parent.attached()
Child.attached()
Note: attaching() and child activation run in parallel
ATTACHED() AWAITS ATTACHING()
═══════════════════════════════════════════════════════════
export class MyComponent {
async attaching() {
await animateIn(); // ← Async
}
attached() {
// Only called AFTER attaching() resolves
console.log('Animation complete!');
}
}
Timeline:
attaching() ──────┐
├─ await animation
└──→ [ animation completes ]
↓
attached() ← Called now
This ensures you can safely measure DOM in attached()
DETACHING() PARALLEL BEHAVIOR
═══════════════════════════════════════════════════════════
Detaching hooks await in PARALLEL:
export class Parent {
async detaching() {
await this.animateOut(); // 500ms
}
}
export class Child1 {
async detaching() {
await this.animateOut(); // 300ms
}
}
export class Child2 {
async detaching() {
await this.animateOut(); // 400ms
}
}
Timeline:
Parent.detaching() ────────┐ (500ms)
Child1.detaching() ─────┐ │ (300ms)
Child2.detaching() ──────┤ │ (400ms)
│ │
│ │ (all run in parallel)
│ │
All complete ────────────┴──┘
↓ (after 500ms - longest)
removeNodes()
unbinding()
Total time: 500ms (not 1200ms!)
PROMISE REJECTION HANDLING
═══════════════════════════════════════════════════════════
If a lifecycle hook Promise rejects:
export class MyComponent {
async binding() {
throw new Error('Failed to load data');
}
}
Behavior:
ret.catch((err: Error) => {
this._reject(err); // Propagates to controller.$promise
});
The activation aborts and the error propagates to the parent.
The component will NOT be activated.
BEST PRACTICES
═══════════════════════════════════════════════════════════
DO use async in attaching() for animations
(runs in parallel, doesn't block)
DO use async in detaching() for exit animations
(all run in parallel)
AVOID async in binding() unless necessary
(blocks all children from starting)
DO use loading() for data fetching
(router lifecycle, doesn't block children)
AVOID long-running operations in binding()
(delays entire component tree activation)5. Common Pitfalls
Real-world mistakes and how to avoid them:
PITFALL #1: Memory Leaks from Event Listeners
═══════════════════════════════════════════════════════════
BAD - Leaks memory:
export class MyComponent {
attached() {
window.addEventListener('resize', this.handleResize);
}
// Missing cleanup!
}
GOOD - Properly cleaned up:
export class MyComponent {
attached() {
window.addEventListener('resize', this.handleResize);
}
detaching() {
window.removeEventListener('resize', this.handleResize);
}
}
BETTER - Use bound method:
export class MyComponent {
private handleResize = () => { /* ... */ };
attached() {
window.addEventListener('resize', this.handleResize);
}
detaching() {
window.removeEventListener('resize', this.handleResize);
}
}
PITFALL #2: Accessing DOM Before It's Ready
═══════════════════════════════════════════════════════════
BAD - DOM not ready:
export class MyComponent {
binding() {
// DOM not attached yet!
const width = this.element.offsetWidth; // Might be 0
}
}
GOOD - Wait for attached:
export class MyComponent {
attached() {
// DOM is now in document and laid out
const width = this.element.offsetWidth; // Correct!
}
}
Why: binding() happens before DOM is attached.
Use attached() for DOM measurements.
PITFALL #3: Blocking Children with Slow binding()
═══════════════════════════════════════════════════════════
BAD - Blocks entire tree:
export class Parent {
async binding() {
// This delays ALL children for 2 seconds!
this.data = await slowApiCall(); // 2000ms
}
}
GOOD - Use loading() or attached():
export class Parent {
async loading() {
// Children can start while this runs
this.data = await slowApiCall();
}
binding() {
// Quick, synchronous setup only
}
}
Or if not using router:
export class Parent {
binding() {
// Synchronous setup
}
attached() {
// Async data loading after activation
void this.loadData();
}
private async loadData() {
this.data = await slowApiCall();
}
}
PITFALL #4: Not Awaiting Async Hooks
═══════════════════════════════════════════════════════════
BAD - Missing await:
export class MyComponent {
detaching() {
this.animateOut(); // Missing await/return!
}
private async animateOut() {
await animation.play();
}
}
// Animation cut short because DOM removed immediately!
GOOD - Properly awaited:
export class MyComponent {
detaching() {
return this.animateOut(); // Return the Promise
}
private async animateOut() {
await animation.play();
}
}
// Framework waits for animation before removing DOM
PITFALL #5: Heavy Work in Constructor
═══════════════════════════════════════════════════════════
BAD - Premature work:
export class MyComponent {
@bindable data: any;
constructor() {
// data is undefined! Bindables not set yet
this.processData(this.data); // undefined!
}
}
GOOD - Wait for binding:
export class MyComponent {
@bindable data: any;
binding() {
// Bindables are now set
this.processData(this.data); // Correct!
}
}
Rule: Constructor runs before bindables are set.
Use binding() or later hooks to access bindables.
PITFALL #6: Forgetting dispose() for Long-Lived Resources
═══════════════════════════════════════════════════════════
BAD - Resource leak:
export class MyComponent {
private subscription: Subscription;
attached() {
this.subscription = eventAggregator.subscribe('event', this.handler);
}
detaching() {
this.subscription.dispose(); // Not enough!
}
}
// If component is cached (repeat.for), subscription persists!
GOOD - Clean up in dispose:
export class MyComponent {
private subscription: Subscription;
attached() {
this.subscription = eventAggregator.subscribe('event', this.handler);
}
detaching() {
// Short-lived cleanup
}
dispose() {
// Permanent cleanup
this.subscription?.dispose();
}
}
When to use each:
- detaching(): Temporary deactivation (might reactivate)
- dispose(): Permanent cleanup (never coming back)
PITFALL #7: Modifying @observable During Deactivation
═══════════════════════════════════════════════════════════
BAD - Triggers bindings during teardown:
export class MyComponent {
@observable isActive: boolean = true;
unbinding() {
this.isActive = false; // Triggers change handlers!
}
}
// Can cause errors if bindings partially disconnected
GOOD - Set state before unbinding:
export class MyComponent {
@observable isActive: boolean = true;
detaching() {
// Bindings still active, safe to modify
this.isActive = false;
}
unbinding() {
// Just cleanup, no state changes
}
}
PITFALL #8: Not Handling Deactivation During Activation
═══════════════════════════════════════════════════════════
BAD - Race condition:
export class MyComponent {
private data: any;
async binding() {
this.data = await fetch('/api/slow'); // 5 seconds
// User navigates away after 1 second...
this.doSomething(this.data); // Component might be gone!
}
}
GOOD - Check state:
export class MyComponent {
private data: any;
private isActive = true;
async binding() {
this.data = await fetch('/api/slow');
if (!this.isActive) {
return; // Don't continue if deactivated
}
this.doSomething(this.data);
}
unbinding() {
this.isActive = false;
}
}
BETTER - Use AbortController:
export class MyComponent {
private abortController = new AbortController();
async binding() {
try {
const data = await fetch('/api/slow', {
signal: this.abortController.signal
});
this.doSomething(data);
} catch (err) {
if (err.name === 'AbortError') {
return; // Deactivated, ignore
}
throw err;
}
}
unbinding() {
this.abortController.abort();
}
}
PITFALL #9: Incorrect Parent-Child Communication Timing
═══════════════════════════════════════════════════════════
BAD - Child calls parent too early:
export class Child {
@bindable onReady: () => void;
binding() {
this.onReady(); // Parent might not be bound yet!
}
}
GOOD - Wait for attached:
export class Child {
@bindable onReady: () => void;
attached() {
this.onReady(); // Parent is definitely attached
}
}
Timeline:
Parent.binding()
-> Child.binding() (Too early to communicate up)
-> Child.bound()
-> Parent.bound()
-> Child.attached() (Safe to communicate up)
-> Parent.attached()
PITFALL #10: 3rd Party Library Lifecycle Mismatch
═══════════════════════════════════════════════════════════
BAD - Library not ready:
export class ChartComponent {
binding() {
// DOM not in document yet!
this.chart = new Chart(this.canvasElement); // Might fail
}
}
GOOD - Initialize in attached:
export class ChartComponent {
private chart: Chart | null = null;
attached() {
// DOM is in document and measured
this.chart = new Chart(this.canvasElement);
}
detaching() {
// Clean up before DOM removal
this.chart?.destroy();
this.chart = null;
}
}
Many libraries need:
1. Element in DOM (use attached)
2. Measured layout (use attached)
3. Cleanup before removal (use detaching)
QUICK REFERENCE: Which Hook For What?
═══════════════════════════════════════════════════════════
Task Hook
─────────────────────────────────────────────────────────
Inject services constructor
Access @bindable values binding or later
Fetch data (router) loading
Fetch data (no router) attached
Set up DOM listeners attached
Initialize 3rd party library attached
Measure DOM elements attached
Start animations attaching
Exit animations detaching
Remove DOM listeners detaching
Clean up 3rd party library detaching
Dispose long-lived subscriptions dispose
Avoid async here binding (blocks children)
Async OK here attaching, detaching, attachedSummary
Key Takeaways:
Activation is top-down until attached (which is bottom-up)
Deactivation is bottom-up throughout
binding() blocks children, attaching() doesn't
Stacks coordinate timing between parent and children
Always clean up in the opposite hook (attached ↔ detaching)
Use attached() for DOM work, not binding()
For more details, see the main Component Lifecycles documentation.
Last updated
Was this helpful?