Skip to content

Commit 8eb6677

Browse files
committed
docs: Create 'Async Destruction & The Trap Pattern' Guide (#8806)
1 parent 30acdd7 commit 8eb6677

2 files changed

Lines changed: 134 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Async Destruction & The Trap Pattern
2+
3+
In single-page applications, handling the lifecycle of asynchronous operations is a critical challenge. A component might trigger a network request (like `fetch`) or a dynamic import, but be destroyed by the user (e.g., navigating away) before that operation completes.
4+
5+
If the callback for that operation attempts to modify the component's state (e.g., `this.setState()`, `this.vdom = ...`), it will likely throw an error because the instance no longer exists or is in a broken state. Worse, it can lead to memory leaks or "zombie" processes that consume resources unnecessarily.
6+
7+
Neo.mjs provides a robust, built-in mechanism to handle this: **The Trap Pattern**.
8+
9+
## The Problem: Zombie Callbacks
10+
11+
Consider this common scenario in a component or controller:
12+
13+
```javascript
14+
// BAD: Unsafe async operation
15+
async loadData() {
16+
// 1. Start a network request
17+
const response = await fetch('/api/data');
18+
const data = await response.json();
19+
20+
// 2. DANGER ZONE:
21+
// If the component was destroyed while awaiting above,
22+
// 'this' might be destroyed. Calling setter methods or
23+
// triggering updates will throw an error.
24+
this.items = data;
25+
}
26+
```
27+
28+
If `this.destroy()` is called while `fetch` is pending, the execution resumes after the `await` on a dead instance.
29+
30+
## The Solution: `this.trap()`
31+
32+
`Neo.core.Base`, the ancestor of almost every class in the framework (Components, Controllers, Stores), implements a method called `trap()`.
33+
34+
`trap()` acts as a lifecycle-aware guard for Promises.
35+
36+
1. It wraps the Promise you pass to it.
37+
2. If the Promise resolves **while the component is alive**, it returns the result normally.
38+
3. If the component is **destroyed** (or currently destroying) before the Promise resolves, `trap()` **rejects** the Promise with a specific symbol: `Neo.isDestroyed`.
39+
40+
## Usage Examples
41+
42+
### Basic Fetch
43+
44+
Here is the corrected version of the previous example using `trap()`:
45+
46+
```javascript
47+
// GOOD: Trapped async operation
48+
async loadData() {
49+
let me = this, // 'me' reference is standard in Neo.mjs
50+
data;
51+
52+
try {
53+
// Wrap the fetch promise
54+
const response = await me.trap(fetch('/api/data'));
55+
56+
// Wrap the json parsing promise
57+
data = await me.trap(response.json());
58+
59+
// Safe to use 'me' here, because trap() ensured we are still alive
60+
me.items = data;
61+
62+
} catch (err) {
63+
// Gracefully handle the destruction case
64+
if (err !== Neo.isDestroyed) {
65+
console.error('Real error occurred:', err);
66+
}
67+
// If err === Neo.isDestroyed, we just stop.
68+
// No further code executes, effectively killing the "zombie" logic.
69+
}
70+
}
71+
```
72+
73+
### Dynamic Imports
74+
75+
Dynamic imports are also asynchronous and should be trapped if the loaded module is used to update the instance.
76+
77+
```javascript
78+
async loadChartEngine() {
79+
let me = this,
80+
module;
81+
82+
try {
83+
// If the user closes the view while the chart engine is downloading,
84+
// this will reject, and we won't try to instantiate a chart on a dead view.
85+
module = await me.trap(import('amcharts/amcharts4.mjs'));
86+
87+
me.chartEngine = module.default;
88+
me.renderChart();
89+
90+
} catch (err) {
91+
if (err !== Neo.isDestroyed) {
92+
console.error('Failed to load chart engine', err);
93+
}
94+
}
95+
}
96+
```
97+
98+
### Promise.all
99+
100+
You can trap combined promises as well.
101+
102+
```javascript
103+
async loadAllData() {
104+
let me = this;
105+
106+
try {
107+
const [users, projects] = await me.trap(Promise.all([
108+
fetch('/api/users').then(r => r.json()),
109+
fetch('/api/projects').then(r => r.json())
110+
]));
111+
112+
me.store.users = users;
113+
me.store.projects = projects;
114+
115+
} catch (err) {
116+
if (err !== Neo.isDestroyed) {
117+
throw err;
118+
}
119+
}
120+
}
121+
```
122+
123+
## Best Practices
124+
125+
1. **Trap Early, Trap Often**: Wrap *every* asynchronous boundary that crosses into `this` context access.
126+
2. **Separate `fetch` and `json()`**: Both are async points. It is safest to trap them individually or chain them inside a single trapped promise if preferred.
127+
3. **Check `Neo.isDestroyed`**: In your `catch` block, always check if the error is `Neo.isDestroyed` to silence expected lifecycle interruptions.
128+
4. **Controllers**: `Neo.controller.Base` inherits from `core.Base`, so ViewControllers have full access to `trap()`. This is the most common place to use it for fetching view data.
129+
5. **Stores**: `Neo.data.Store` uses `trap()` internally for its `load()` method, but if you are implementing custom data loading logic in a store, you should use it too.
130+
131+
## Architecture Note
132+
133+
The `Neo.isDestroyed` symbol is globally available (assigned to `Neo` in `src/Neo.mjs`). The global error handler in `src/Neo.mjs` is also configured to ignore unhandled rejections if the reason is `Neo.isDestroyed`, ensuring your console remains clean of "Uncaught (in promise)" errors for valid lifecycle cancellations.

learn/tree.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
{"name": "Application Bootstrap", "parentId": "guides/fundamentals", "id": "guides/fundamentals/ApplicationBootstrap"},
3737
{"name": "Instance Lifecycle", "parentId": "guides/fundamentals", "id": "guides/fundamentals/InstanceLifecycle"},
3838
{"name": "Worker Architecture & Messaging", "parentId": "guides/fundamentals", "id": "guides/fundamentals/WorkerArchitecture"},
39+
{"name": "Async Destruction & The Trap Pattern", "parentId": "guides/fundamentals", "id": "guides/fundamentals/AsyncDestruction"},
3940
{"name": "Declarative Component Trees VS Imperative Vdom", "parentId": "guides/fundamentals", "id": "guides/fundamentals/DeclarativeComponentTreesVsImperativeVdom"},
4041
{"name": "Declarative VDOM with Effects", "parentId": "guides/fundamentals", "id": "guides/fundamentals/DeclarativeVDOMWithEffects"},
4142
{"name": "Config System Deep Dive", "parentId": "guides/fundamentals", "id": "guides/fundamentals/ConfigSystemDeepDive"},

0 commit comments

Comments
 (0)