@b9g/crank 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,61 @@
1
- ## Try Crank
1
+ <div align="center">
2
+ <img src="logo.svg" alt="Crank.js Logo" width="200" height="200" />
2
3
 
3
- The fastest way to try Crank is via the [online playground](https://crank.js.org/playground). In addition, many of the code examples in these guides feature live previews.
4
+ # Crank.js
5
+ The Just JavaScript Framework
6
+ </div>
7
+
8
+ ## Get Started
9
+
10
+ The fastest way to try Crank is via the [online playground](https://crank.js.org/playground).
11
+
12
+ Other links:
13
+ - [crank.js.org](https://crank.js.org)
14
+ - [Deep Wiki](https://deepwiki.com/bikeshaving/crank)
15
+ - [Examples](https://github.com/bikeshaving/crank/tree/main/examples)
16
+
17
+ ## Motivation
18
+ **A framework that feels like JavaScript.**
19
+
20
+ While other frameworks invent new paradigms and force you to learn
21
+ framework-specific APIs, Crank embraces the language features you already know.
22
+ No hooks to memorize, no dependency arrays to debug, no cache invalidation to
23
+ manage.
24
+
25
+ ### Pure JavaScript, No Compromises
26
+
27
+ ```javascript
28
+ // Async components just work
29
+ async function UserProfile({userId}) {
30
+ const user = await fetchUser(userId);
31
+ return <div>Hello, {user.name}!</div>;
32
+ }
33
+
34
+ // Lifecycle logic with generators feels natural
35
+ function* Timer() {
36
+ let seconds = 0;
37
+ const interval = setInterval(() => this.refresh(() => seconds++), 1000);
38
+ for ({} of this) {
39
+ yield <div>Seconds: {seconds}</div>;
40
+ }
41
+ clearInterval(interval); // Cleanup just works
42
+ }
43
+ ```
44
+
45
+ ### Why Developers Choose Crank
46
+
47
+ - **Intuitive**: Use `async`/`await` for loading states and `function*` for lifecycles — no new APIs to learn
48
+ - **Fast**: Outperforms React in benchmarks while weighing just 5KB with zero dependencies
49
+ - **Flexible**: Write components in vanilla JavaScript with template literals, or use JSX
50
+ - **Clean**: State lives in function scope, lifecycle code goes where it belongs, no mysterious re-render bugs
51
+ - **Future-proof**: Built on stable JavaScript features, not evolving framework abstractions
52
+
53
+ ### The "Just JavaScript" Promise, Delivered
54
+
55
+ Other frameworks claim to be "just JavaScript" but ask you to think in terms of
56
+ effects, dependencies, and framework-specific patterns. Crank actually delivers
57
+ on that promise — your components are literally just functions that use standard
58
+ JavaScript control flow.
4
59
 
5
60
  ## Installation
6
61
 
@@ -12,20 +67,6 @@ b*ikeshavin*g).
12
67
  npm i @b9g/crank
13
68
  ```
14
69
 
15
- ### Importing Crank with the **classic** JSX transform.
16
-
17
- ```jsx live
18
- /** @jsx createElement */
19
- /** @jsxFrag Fragment */
20
- import {createElement, Fragment} from "@b9g/crank";
21
- import {renderer} from "@b9g/crank/dom";
22
-
23
- renderer.render(
24
- <p>This paragraph element is transpiled with the classic transform.</p>,
25
- document.body,
26
- );
27
- ```
28
-
29
70
  ### Importing Crank with the **automatic** JSX transform.
30
71
 
31
72
  ```jsx live
@@ -38,11 +79,12 @@ renderer.render(
38
79
  );
39
80
  ```
40
81
 
41
- You will likely have to configure your tools to support JSX, especially if you do not want to use `@jsx` comment pragmas. See below for common tools and configurations.
42
-
43
82
  ### Importing the JSX template tag.
44
83
 
45
- Starting in version `0.5`, the Crank package ships a [tagged template function](/guides/jsx-template-tag) which provides similar syntax and semantics as the JSX transform. This allows you to write Crank components in vanilla JavaScript.
84
+ Starting in version `0.5`, the Crank package ships a [tagged template
85
+ function](/guides/jsx-template-tag) which provides similar syntax and semantics
86
+ as the JSX transform. This allows you to write Crank components in vanilla
87
+ JavaScript.
46
88
 
47
89
  ```js live
48
90
  import {jsx} from "@b9g/crank/standalone";
@@ -55,15 +97,11 @@ renderer.render(jsx`
55
97
 
56
98
  ### ECMAScript Module CDNs
57
99
  Crank is also available on CDNs like [unpkg](https://unpkg.com)
58
- (https://unpkg.com/@b9g/crank?module) and [esm.sh](https://esm.sh)
59
- (https://esm.sh/@b9g/crank) for usage in ESM-ready environments.
100
+ (https://unpkg.com/@b9g/crank?module), [esm.sh](https://esm.sh)
101
+ (https://esm.sh/@b9g/crank), and [esm.run](https://esm.run/@b9g/crank) for usage in ESM-ready environments.
60
102
 
61
103
  ```jsx live
62
104
  /** @jsx createElement */
63
-
64
- // This is an ESM-ready environment!
65
- // If code previews work, your browser is an ESM-ready environment!
66
-
67
105
  import {createElement} from "https://unpkg.com/@b9g/crank/crank?module";
68
106
  import {renderer} from "https://unpkg.com/@b9g/crank/dom?module";
69
107
 
@@ -75,6 +113,134 @@ renderer.render(
75
113
  );
76
114
  ```
77
115
 
116
+ ## Key Examples
117
+
118
+ ### A Simple Component
119
+
120
+ ```jsx live
121
+ import {renderer} from "@b9g/crank/dom";
122
+
123
+ function Greeting({name = "World"}) {
124
+ return (
125
+ <div>Hello {name}</div>
126
+ );
127
+ }
128
+
129
+ renderer.render(<Greeting />, document.body);
130
+ ```
131
+
132
+ ### A Stateful Component
133
+
134
+ ```jsx live
135
+ function *Timer(this: Context) {
136
+ let seconds = 0;
137
+ const interval = setInterval(() => this.refresh(() => seconds++), 1000);
138
+ for ({} of this) {
139
+ yield <div>Seconds: {seconds}</div>;
140
+ }
141
+
142
+ clearInterval(interval);
143
+ }
144
+ ```
145
+
146
+ ### An Async Component
147
+
148
+ ```jsx live
149
+ import {renderer} from "@b9g/crank/dom";
150
+ async function Definition({word}) {
151
+ // API courtesy https://dictionaryapi.dev
152
+ const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`);
153
+ const data = await res.json();
154
+ if (!Array.isArray(data)) {
155
+ return <p>No definition found for {word}</p>;
156
+ }
157
+
158
+ const {phonetic, meanings} = data[0];
159
+ const {partOfSpeech, definitions} = meanings[0];
160
+ const {definition} = definitions[0];
161
+ return <>
162
+ <p>{word} <code>{phonetic}</code></p>
163
+ <p><b>{partOfSpeech}.</b> {definition}</p>
164
+ </>;
165
+ }
166
+
167
+ await renderer.render(<Definition word="framework" />, document.body);
168
+ ```
169
+
170
+ ### A Loading Component
171
+
172
+ ```jsx live
173
+ import {Fragment} from "@b9g/crank";
174
+ import {renderer} from "@b9g/crank/dom";
175
+
176
+ async function LoadingIndicator() {
177
+ await new Promise(resolve => setTimeout(resolve, 1000));
178
+ return (
179
+ <div>
180
+ 🐕 Fetching a good boy...
181
+ </div>
182
+ );
183
+ }
184
+
185
+ async function RandomDog({throttle = false}) {
186
+ const res = await fetch("https://dog.ceo/api/breeds/image/random");
187
+ const data = await res.json();
188
+ if (throttle) {
189
+ await new Promise(resolve => setTimeout(resolve, 2000));
190
+ }
191
+
192
+ return (
193
+ <div>
194
+ <a href={data.message} target="_blank" style="text-decoration: none; color: inherit;">
195
+ <img
196
+ src={data.message}
197
+ alt="A Random Dog"
198
+ width="300"
199
+ />
200
+ <div>
201
+ Click to view full size
202
+ </div>
203
+ </a>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ async function *RandomDogLoader({throttle}) {
209
+ // for await can be used to race component trees
210
+ for await ({throttle} of this) {
211
+ yield <LoadingIndicator />;
212
+ yield <RandomDog throttle={throttle} />;
213
+ }
214
+ }
215
+
216
+ function *RandomDogApp() {
217
+ let throttle = false;
218
+ this.addEventListener("click", (ev) => {
219
+ if (ev.target.tagName === "BUTTON") {
220
+ this.refresh(() => throttle = !throttle);
221
+ }
222
+ });
223
+
224
+ for ({} of this) {
225
+ yield (
226
+ <div>
227
+ <RandomDogLoader throttle={throttle} />
228
+ <div>
229
+ <button>
230
+ Show me another dog!
231
+ </button>
232
+ <div>
233
+ {throttle ? "Slow mode" : "Fast mode"}
234
+ </div>
235
+ </div>
236
+ </div>
237
+ );
238
+ }
239
+ }
240
+
241
+ renderer.render(<RandomDogApp />, document.body);
242
+ ```
243
+
78
244
  ## Common tool configurations
79
245
  The following is an incomplete list of configurations to get started with Crank.
80
246
 
@@ -93,28 +259,15 @@ Here’s the configuration you will need to set up automatic JSX transpilation.
93
259
  }
94
260
  ```
95
261
 
96
- The classic transform is supported as well.
97
-
98
- ```tsconfig.json
99
- {
100
- "compilerOptions": {
101
- "jsx": "react",
102
- "jsxFactory": "createElement",
103
- "jsxFragmentFactory": "Fragment"
104
- }
105
- }
106
- ```
107
-
108
- Crank is written in TypeScript. Refer to [the guide on TypeScript](/guides/working-with-typescript) for more information about Crank types.
262
+ Crank is written in TypeScript. Refer to [the guide on
263
+ TypeScript](https://crank.js.org/guides/working-with-typescript) for more
264
+ information about Crank types.
109
265
 
110
266
  ```tsx
111
267
  import type {Context} from "@b9g/crank";
112
268
  function *Timer(this: Context) {
113
269
  let seconds = 0;
114
- const interval = setInterval(() => {
115
- seconds++;
116
- this.refresh();
117
- }, 1000);
270
+ const interval = setInterval(() => this.refresh(() => seconds++), 1000);
118
271
  for ({} of this) {
119
272
  yield <div>Seconds: {seconds}</div>;
120
273
  }
@@ -148,26 +301,6 @@ Automatic transform:
148
301
  }
149
302
  ```
150
303
 
151
- Classic transform:
152
- ```.babelrc.json
153
- {
154
- "plugins": [
155
- "@babel/plugin-syntax-jsx",
156
- [
157
- "@babel/plugin-transform-react-jsx",
158
- {
159
- "runtime": "class",
160
- "pragma": "createElement",
161
- "pragmaFrag": "''",
162
-
163
- "throwIfNamespace": false,
164
- "useSpread": true
165
- }
166
- ]
167
- ]
168
- }
169
- ```
170
-
171
304
  ### [ESLint](https://eslint.org)
172
305
 
173
306
  ESLint is a popular open-source tool for analyzing and detecting problems in JavaScript code.
@@ -208,121 +341,496 @@ export default defineConfig({
208
341
  });
209
342
  ```
210
343
 
211
- ## Key Examples
344
+ ## API Reference
212
345
 
213
- ### A Simple Component
346
+ ### Core Exports
214
347
 
215
- ```jsx live
216
- import {renderer} from "@b9g/crank/dom";
348
+ ```javascript
349
+ import {
350
+ createElement,
351
+ Fragment,
352
+ Copy,
353
+ Portal,
354
+ Raw,
355
+ Text,
356
+ Context
357
+ } from "@b9g/crank";
358
+
359
+ import {renderer} from "@b9g/crank/dom"; // Browser DOM
360
+ import {renderer} from "@b9g/crank/html"; // Server-side HTML
217
361
 
362
+ import {jsx, html} from "@b9g/crank/standalone"; // Template tag (no build)
363
+
364
+ import {Suspense, SuspenseList, lazy} from "@b9g/crank/async";
365
+ ```
366
+
367
+ ---
368
+
369
+ ### Component Types
370
+
371
+ **Function Component** - Stateless
372
+ ```javascript
218
373
  function Greeting({name = "World"}) {
219
- return (
220
- <div>Hello {name}</div>
221
- );
374
+ return <div>Hello {name}</div>;
222
375
  }
376
+ ```
223
377
 
224
- renderer.render(<Greeting />, document.body);
378
+ **Generator Component** - Stateful with `function*`
379
+ ```javascript
380
+ function* Counter() {
381
+ let count = 0;
382
+ const onclick = () => this.refresh(() => count++);
383
+
384
+ for ({} of this) {
385
+ yield <button onclick={onclick}>Count: {count}</button>;
386
+ }
387
+ }
225
388
  ```
226
389
 
227
- ### A Stateful Component
390
+ **Async Component** - Uses `async` for promises
391
+ ```javascript
392
+ async function UserProfile({userId}) {
393
+ const user = await fetch(`/api/users/${userId}`).then(r => r.json());
394
+ return <div>Hello, {user.name}!</div>;
395
+ }
396
+ ```
228
397
 
229
- ```jsx live
230
- import {renderer} from "@b9g/crank/dom";
398
+ **Async Generator Component** - Stateful + async
399
+ ```javascript
400
+ async function* DataLoader({url}) {
401
+ for ({url} of this) {
402
+ const data = await fetch(url).then(r => r.json());
403
+ yield <div>{data.message}</div>;
404
+ }
405
+ }
406
+ ```
231
407
 
232
- function *Timer() {
233
- let seconds = 0;
234
- const interval = setInterval(() => {
235
- seconds++;
236
- this.refresh();
237
- }, 1000);
238
- try {
239
- while (true) {
240
- yield <div>Seconds: {seconds}</div>;
241
- }
242
- } finally {
243
- clearInterval(interval);
408
+ ---
409
+
410
+ ### Context API
411
+
412
+ The context is available as `this` in components (or as 2nd parameter).
413
+
414
+ ```javascript
415
+ function Component(props, ctx) {
416
+ console.log(this === ctx); // true
417
+ return props.children;
418
+ }
419
+ ```
420
+
421
+ #### Properties
422
+
423
+ **`this.props`** - Current props (readonly)
424
+
425
+ **`this.isExecuting`** - Whether the component is currently executing
426
+
427
+ **`this.isUnmounted`** - Whether the component is unmounted
428
+
429
+ #### Methods
430
+
431
+ **`this.refresh(callback?)`** - Trigger re-render
432
+ ```javascript
433
+ this.refresh(); // Simple refresh
434
+ this.refresh(() => count++); // With state update (v0.7+)
435
+ ```
436
+
437
+ **`this.schedule(callback)`** - Execute after render
438
+ ```javascript
439
+ this.schedule(() => {
440
+ console.log("Component rendered");
441
+ });
442
+ ```
443
+
444
+ **`this.cleanup(callback)`** - Register cleanup on unmount
445
+ ```javascript
446
+ function* Component() {
447
+ const interval = setInterval(() => this.refresh(), 1000);
448
+ this.cleanup(() => clearInterval(interval));
449
+
450
+ for ({} of this) {
451
+ yield <div>Tick</div>;
244
452
  }
245
453
  }
454
+ ```
246
455
 
247
- renderer.render(<Timer />, document.body);
456
+ **`this.addEventListener(type, listener, options?)`** - Listen to events
457
+ ```javascript
458
+ this.addEventListener("click", (e) => console.log("Clicked!"));
248
459
  ```
249
460
 
250
- ### An Async Component
461
+ **`this.dispatchEvent(event)`** - Dispatch events
462
+ ```javascript
463
+ this.dispatchEvent(new CustomEvent("mybuttonclick", {
464
+ bubbles: true,
465
+ detail: {id: props.id}
466
+ }));
467
+ ```
251
468
 
252
- ```jsx live
253
- import {renderer} from "@b9g/crank/dom";
254
- async function Definition({word}) {
255
- // API courtesy https://dictionaryapi.dev
256
- const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`);
257
- const data = await res.json();
258
- if (!Array.isArray(data)) {
259
- return <p>No definition found for {word}</p>;
469
+ **`this.provide(key, value)`** / **`this.consume(key)`** - Context API
470
+ ```javascript
471
+ // Provider
472
+ function* ThemeProvider() {
473
+ this.provide("theme", "dark");
474
+ for ({} of this) {
475
+ yield this.props.children;
260
476
  }
477
+ }
261
478
 
262
- const {phonetic, meanings} = data[0];
263
- const {partOfSpeech, definitions} = meanings[0];
264
- const {definition} = definitions[0];
265
- return <>
266
- <p>{word} <code>{phonetic}</code></p>
267
- <p><b>{partOfSpeech}.</b> {definition}</p>
268
- </>;
479
+ // Consumer
480
+ function ThemedButton() {
481
+ const theme = this.consume("theme");
482
+ return <button class={theme}>Click me</button>;
269
483
  }
484
+ ```
270
485
 
271
- await renderer.render(<Definition word="framework" />, document.body);
486
+ #### Iteration
487
+
488
+ **`for ({} of this)`** - Render loop (sync)
489
+ ```javascript
490
+ function* Component() {
491
+ for ({} of this) {
492
+ yield <div>{this.props.message}</div>;
493
+ }
494
+ }
272
495
  ```
273
496
 
274
- ### A Loading Component
497
+ **`for await ({} of this)`** - Async render loop for racing trees
498
+ ```javascript
499
+ async function* AsyncComponent() {
500
+ for await ({} of this) {
501
+ // Multiple yields race - whichever completes first shows
502
+ yield <Loading />;
503
+ yield <Content />;
504
+ }
505
+ }
506
+ ```
275
507
 
276
- ```jsx live
508
+ ---
509
+
510
+ ### Special Props
511
+
512
+ **`key`** - Unique identifier for reconciliation
513
+ ```javascript
514
+ {items.map(item => <li key={item.id}>{item.name}</li>)}
515
+ ```
516
+
517
+ **`ref`** - Access rendered DOM element
518
+ ```javascript
519
+ <audio ref={(el) => (audio = el)} />
520
+
521
+ // Forward refs through components
522
+ function MyInput({ref, ...props}) {
523
+ return <input ref={ref} {...props} />;
524
+ }
525
+ ```
526
+
527
+ **`copy`** - Prevent/control re-rendering
528
+ ```javascript
529
+ // Boolean: prevent rendering when truthy
530
+ <li copy={!el.hasChanged}>{el.value}</li>
531
+
532
+ // string: copy specific props
533
+ <input copy="!value" type="text" /> // Copy all except value
534
+ <div copy="class id" /> // Copy only class and id
535
+ <div copy="children" /> // Copy children
536
+ ```
537
+
538
+ **`hydrate`** - Control SSR hydration
539
+ ```javascript
540
+ <div hydrate={false}> // Skip hydration
541
+ <Portal hydrate={true}> // Force hydration
542
+ <input hydrate="!value" /> // Hydrate all except value
543
+ ```
544
+
545
+ **`class`** - String or object (v0.7+)
546
+ ```javascript
547
+ <button class="btn active" />
548
+
549
+ <button class={{
550
+ btn: true,
551
+ 'btn-active': isActive,
552
+ 'btn-disabled': isDisabled
553
+ }} />
554
+ ```
555
+
556
+ **`style`** - CSS string or object
557
+ ```javascript
558
+ <div style="color: red; font-size: 16px" />
559
+ <div style={{"font-size": "16px", color: "blue"}} />
560
+ ```
561
+
562
+ **`innerHTML`** - Raw HTML string (⚠️ XSS risk)
563
+ ```javascript
564
+ <div innerHTML="<strong>Bold</strong>" />
565
+ ```
566
+
567
+ **Event Props** - Lowercase event handlers
568
+ ```javascript
569
+ <button onclick={handler} />
570
+ <input onchange={handler} oninput={handler} />
571
+ <form onsubmit={handler} />
572
+ ```
573
+
574
+ **Prop Naming** - HTML-friendly names supported
575
+ ```javascript
576
+ <label class="my-label" for="my-id">Label</label>
577
+ // Instead of className and htmlFor
578
+ ```
579
+
580
+ ---
581
+
582
+ ### Special Element Tags
583
+
584
+ **`<Fragment>`** - Render children without wrapper
585
+ ```javascript
277
586
  import {Fragment} from "@b9g/crank";
278
- import {renderer} from "@b9g/crank/dom";
279
587
 
280
- async function LoadingIndicator() {
281
- await new Promise(resolve => setTimeout(resolve, 1000));
282
- return <div>Fetching a good boy...</div>;
588
+ <Fragment>
589
+ <div>Child 1</div>
590
+ <div>Child 2</div>
591
+ </Fragment>
592
+
593
+ // Or use: <>...</>
594
+ // The Fragment tag is the empty string
595
+ ```
596
+
597
+ **`<Copy />`** - Prevent element re-rendering
598
+ ```javascript
599
+ import {Copy} from "@b9g/crank";
600
+
601
+ function memo(Component) {
602
+ return function* Wrapped(props) {
603
+ yield <Component {...props} />;
604
+ for (const newProps of this) {
605
+ if (equals(props, newProps)) {
606
+ yield <Copy />; // Reuse previous render
607
+ } else {
608
+ yield <Component {...newProps} />;
609
+ }
610
+ props = newProps;
611
+ }
612
+ };
283
613
  }
614
+ ```
284
615
 
285
- async function RandomDog({throttle = false}) {
286
- const res = await fetch("https://dog.ceo/api/breeds/image/random");
287
- const data = await res.json();
288
- if (throttle) {
289
- await new Promise(resolve => setTimeout(resolve, 2000));
290
- }
616
+ **`<Portal>`** - Render into different DOM node
617
+ ```javascript
618
+ import {Portal} from "@b9g/crank";
619
+
620
+ const modalRoot = document.getElementById("modal-root");
291
621
 
622
+ function Modal({children}) {
292
623
  return (
293
- <a href={data.message}>
294
- <img src={data.message} alt="A Random Dog" width="300" />
295
- </a>
624
+ <Portal root={modalRoot}>
625
+ <div class="modal">
626
+ {children}
627
+ </div>
628
+ </Portal>
296
629
  );
297
630
  }
631
+ ```
298
632
 
299
- async function *RandomDogLoader({throttle}) {
300
- for await ({throttle} of this) {
301
- yield <LoadingIndicator />;
302
- yield <RandomDog throttle={throttle} />;
303
- }
633
+ **`<Raw>`** - Insert raw HTML or DOM nodes
634
+ ```javascript
635
+ import {Raw} from "@b9g/crank";
636
+
637
+ function MarkdownViewer({markdown}) {
638
+ const html = marked(markdown);
639
+ return <div><Raw value={html} /></div>;
304
640
  }
305
641
 
306
- function *RandomDogApp() {
307
- let throttle = false;
308
- this.addEventListener("click", (ev) => {
309
- if (ev.target.tagName === "BUTTON") {
310
- throttle = !throttle;
311
- this.refresh();
642
+ // Or insert DOM node
643
+ <Raw value={domNode} />
644
+ ```
645
+
646
+ **`<Text>`** - Explicit text node creation (v0.7+)
647
+ ```javascript
648
+ import {Text} from "@b9g/crank";
649
+
650
+ <Text value="Hello world" />
651
+
652
+ // Access Text nodes in lifecycle
653
+ function* Component() {
654
+ this.schedule((node) => {
655
+ if (node instanceof Text) {
656
+ console.log("Text node:", node);
312
657
  }
313
658
  });
659
+ for ({} of this) {
660
+ yield "Text content"; // Becomes a Text node
661
+ }
662
+ }
663
+ ```
664
+
665
+ ---
666
+
667
+ ### Async Utilities (v0.7+)
668
+
669
+ **`lazy(loader)`** - Lazy-load components
670
+ ```javascript
671
+ import {lazy} from "@b9g/crank/async";
672
+
673
+ const LazyComponent = lazy(() => import("./MyComponent.js"));
674
+
675
+ <Suspense fallback={<div>Loading...</div>}>
676
+ <LazyComponent />
677
+ </Suspense>
678
+ ```
679
+
680
+ **`Suspense`** - Declarative loading states
681
+ ```javascript
682
+ import {Suspense} from "@b9g/crank/async";
683
+
684
+ <Suspense fallback={<div>Loading...</div>}>
685
+ <AsyncComponent />
686
+ </Suspense>
687
+ ```
688
+
689
+ **`SuspenseList`** - Coordinate multiple async components
690
+ ```javascript
691
+ import {SuspenseList} from "@b9g/crank/async";
692
+
693
+ <SuspenseList>
694
+ <Suspense fallback={<div>Loading 1...</div>}>
695
+ <Item1 />
696
+ </Suspense>
697
+ <Suspense fallback={<div>Loading 2...</div>}>
698
+ <Item2 />
699
+ </Suspense>
700
+ </SuspenseList>
701
+ ```
702
+
703
+ ---
704
+
705
+ ### Lifecycle Patterns
706
+
707
+ **Mount** - Code before first `yield`
708
+ ```javascript
709
+ function* Component() {
710
+ console.log("Mounting...");
711
+ const interval = setInterval(() => this.refresh(), 1000);
314
712
 
315
713
  for ({} of this) {
316
- yield (
317
- <Fragment>
318
- <RandomDogLoader throttle={throttle} />
319
- <p>
320
- <button>Show me another dog.</button>
321
- </p>
322
- </Fragment>
323
- );
714
+ yield <div>Tick</div>;
324
715
  }
716
+
717
+ clearInterval(interval); // Cleanup
325
718
  }
719
+ ```
326
720
 
327
- renderer.render(<RandomDogApp />, document.body);
721
+ **Update** - Code inside render loop
722
+ ```javascript
723
+ function* Component() {
724
+ for ({} of this) {
725
+ console.log("Updated with:", this.props);
726
+ yield <div>{this.props.message}</div>;
727
+ }
728
+ }
328
729
  ```
730
+
731
+ **Cleanup** - Code after loop or via `this.cleanup()`
732
+ ```javascript
733
+ function* Component() {
734
+ const interval = setInterval(() => this.refresh(), 1000);
735
+ this.cleanup(() => clearInterval(interval));
736
+
737
+ for ({} of this) {
738
+ yield <div>Tick</div>;
739
+ }
740
+ }
741
+ ```
742
+
743
+ ---
744
+
745
+ ### Advanced Patterns
746
+
747
+ **Higher-Order Components**
748
+ ```javascript
749
+ function withLogger(Component) {
750
+ return function* WrappedComponent(props) {
751
+ console.log("Rendering with:", props);
752
+ for ({} of this) {
753
+ yield <Component {...props} />;
754
+ }
755
+ };
756
+ }
757
+ ```
758
+
759
+ **Hooks**
760
+ ```javascript
761
+ function useInterval(ctx, callback, delay) {
762
+ let interval = setInterval(callback, delay);
763
+ ctx.cleanup(() => clearInterval(interval);
764
+ return (newDelay) => {
765
+ delay = newDelay;
766
+ clearInterval(interval);
767
+ interval = setInterval(callback, delay);
768
+ };
769
+ }
770
+ ```
771
+
772
+ **Context Extensions** (⚠️ Prefer hooks over global extensions)
773
+ ```javascript
774
+ import {Context} from "@b9g/crank";
775
+
776
+ Context.prototype.setInterval = function(callback, delay) {
777
+ const interval = setInterval(callback, delay);
778
+ this.cleanup(() => clearInterval(interval));
779
+ };
780
+
781
+ // Use in components
782
+ function* Timer() {
783
+ let seconds = 0;
784
+ this.setInterval(() => this.refresh(() => seconds++), 1000);
785
+
786
+ for ({} of this) {
787
+ yield <div>Seconds: {seconds}</div>;
788
+ }
789
+ }
790
+ ```
791
+
792
+ **Racing Components**
793
+ ```javascript
794
+ async function* DataComponent({url}) {
795
+ for await ({url} of this) {
796
+ yield <Spinner />;
797
+ yield <Data data={data} />;
798
+ }
799
+ }
800
+ ```
801
+
802
+ ---
803
+
804
+ ### TypeScript Support
805
+
806
+ ```typescript
807
+ import type {Context} from "@b9g/crank";
808
+ import {ComponentProps} from "@b9g/crank"; // v0.7+
809
+
810
+ // Component with typed props
811
+ interface Props {
812
+ name: string;
813
+ age?: number;
814
+ }
815
+
816
+ function Greeting({name, age}: Props) {
817
+ return <div>Hello {name}, age {age}</div>;
818
+ }
819
+
820
+ // Generator with typed context
821
+ function* Greeting(this: Context<typeof Greeting>, {name}: {name: string}) {
822
+ for ({name} of this) {
823
+ yield <div>Hello {name}</div>;
824
+ }
825
+ }
826
+
827
+ // Extract component props type
828
+ function Button({variant}: {variant: "primary" | "secondary"}) {
829
+ return <button class={`btn-${variant}`}>Click</button>;
830
+ }
831
+
832
+ type ButtonProps = ComponentProps<typeof Button>;
833
+ ```
834
+
835
+ For comprehensive guides and documentation, visit [crank.js.org](https://crank.js.org)
836
+