mono-jsx 0.4.0 → 0.6.0

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
@@ -5,10 +5,12 @@
5
5
  mono-jsx is a JSX runtime that renders `<html>` element to `Response` object in JavaScript runtimes like Node.js, Deno, Bun, Cloudflare Workers, etc.
6
6
 
7
7
  - 🚀 No build step needed
8
- - 🦋 Lightweight (8KB gzipped), zero dependencies
9
- - 🔫 Minimal state runtime
10
- - 🚨 Complete Web API TypeScript definitions
8
+ - 🦋 Lightweight (10KB gzipped), zero dependencies
9
+ - 🚦 Signals as reactive primitives
10
+ - ⚡️ Use web comonents, no virtual DOM
11
+ - 💡 Complete Web API TypeScript definitions
11
12
  - ⏳ Streaming rendering
13
+ - 🗂️ Built-in router
12
14
  - 🥷 [htmx](#using-htmx) integration
13
15
  - 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
14
16
 
@@ -72,8 +74,8 @@ export default {
72
74
  <html>
73
75
  <h1>Welcome to mono-jsx!</h1>
74
76
  </html>
75
- ),
76
- };
77
+ )
78
+ }
77
79
  ```
78
80
 
79
81
  For Deno/Bun users, you can run the `app.tsx` directly:
@@ -83,13 +85,13 @@ deno serve app.tsx
83
85
  bun run app.tsx
84
86
  ```
85
87
 
86
- If you're building a web app with [Cloudflare Workers](https://developers.cloudflare.com/workers/wrangler/commands/#dev), use `wrangler dev` to start local development:
88
+ If you're building a web app with [Cloudflare Workers](https://developers.cloudflare.com/workers/wrangler/commands/#dev), use `wrangler dev` to start your app in development mode:
87
89
 
88
90
  ```bash
89
91
  npx wrangler dev app.tsx
90
92
  ```
91
93
 
92
- **Node.js doesn't support JSX syntax or declarative fetch servers**, so we recommend using mono-jsx with [srvx](https://srvx.h3.dev/):
94
+ **Node.js doesn't support JSX syntax or declarative fetch servers**, we recommend using mono-jsx with [srvx](https://srvx.h3.dev/):
93
95
 
94
96
  ```tsx
95
97
  // app.tsx
@@ -106,14 +108,14 @@ serve({
106
108
  });
107
109
  ```
108
110
 
109
- You'll need [tsx](https://www.npmjs.com/package/tsx) to start the app without a build step:
111
+ And you'll need [tsx](https://www.npmjs.com/package/tsx) to start the app without a build step:
110
112
 
111
113
  ```bash
112
114
  npx tsx app.tsx
113
115
  ```
114
116
 
115
117
  > [!NOTE]
116
- > Only root `<html>` element will be rendered as a `Response` object. You cannot return a `<div>` or any other element directly from the `fetch` handler. This is a limitation of the mono-jsx runtime.
118
+ > Only root `<html>` element will be rendered as a `Response` object. You cannot return a `<div>` or any other element directly from the `fetch` handler. This is a limitation of the mono-jsx.
117
119
 
118
120
  ## Using JSX
119
121
 
@@ -131,7 +133,7 @@ mono-jsx adopts standard HTML property names, avoiding React's custom naming con
131
133
 
132
134
  mono-jsx allows you to compose the `class` property using arrays of strings, objects, or expressions:
133
135
 
134
- ```jsx
136
+ ```tsx
135
137
  <div
136
138
  class={[
137
139
  "container box",
@@ -145,7 +147,7 @@ mono-jsx allows you to compose the `class` property using arrays of strings, obj
145
147
 
146
148
  mono-jsx supports [pseudo classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes), [pseudo elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements), [media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries), and [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting) in the `style` property:
147
149
 
148
- ```jsx
150
+ ```tsx
149
151
  <a
150
152
  style={{
151
153
  color: "black",
@@ -164,7 +166,7 @@ mono-jsx supports [pseudo classes](https://developer.mozilla.org/en-US/docs/Web/
164
166
 
165
167
  mono-jsx uses [`<slot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) elements to render slotted content (equivalent to React's `children` property). You can also add the `name` attribute to define named slots:
166
168
 
167
- ```jsx
169
+ ```tsx
168
170
  function Container() {
169
171
  return (
170
172
  <div class="container">
@@ -173,7 +175,7 @@ function Container() {
173
175
  {/* Named slot */}
174
176
  <slot name="desc" />
175
177
  </div>
176
- );
178
+ )
177
179
  }
178
180
 
179
181
  function App() {
@@ -184,7 +186,7 @@ function App() {
184
186
  {/* This goes to the default slot */}
185
187
  <h1>Hello world!</h1>
186
188
  </Container>
187
- );
189
+ )
188
190
  }
189
191
  ```
190
192
 
@@ -192,7 +194,7 @@ function App() {
192
194
 
193
195
  mono-jsx provides an `html` tag function to render raw HTML in JSX instead of React's `dangerouslySetInnerHTML`:
194
196
 
195
- ```jsx
197
+ ```tsx
196
198
  function App() {
197
199
  return <div>{html`<h1>Hello world!</h1>`}</div>;
198
200
  }
@@ -200,14 +202,14 @@ function App() {
200
202
 
201
203
  The `html` tag function is globally available without importing. You can also use `css` and `js` tag functions for CSS and JavaScript:
202
204
 
203
- ```jsx
205
+ ```tsx
204
206
  function App() {
205
207
  return (
206
208
  <head>
207
209
  <style>{css`h1 { font-size: 3rem; }`}</style>
208
210
  <script>{js`console.log("Hello world!")`}</script>
209
211
  </head>
210
- );
212
+ )
211
213
  }
212
214
  ```
213
215
 
@@ -218,13 +220,13 @@ function App() {
218
220
 
219
221
  mono-jsx lets you write event handlers directly in JSX, similar to React:
220
222
 
221
- ```jsx
223
+ ```tsx
222
224
  function Button() {
223
225
  return (
224
226
  <button onClick={(evt) => alert("BOOM!")}>
225
227
  Click Me
226
228
  </button>
227
- );
229
+ )
228
230
  }
229
231
  ```
230
232
 
@@ -234,9 +236,10 @@ function Button() {
234
236
  ```tsx
235
237
  import { doSomething } from "some-library";
236
238
 
237
- function Button(this: FC, props: { role: string }) {
238
- let message = "BOOM!";
239
- console.log(message); // only executes on server-side
239
+ function Button(this: FC<{ count: 0 }>, props: { role: string }) {
240
+ const message = "BOOM!"; // server-side variable
241
+ this.count = 0; // initialize a signal
242
+ console.log(message); // only prints on server-side
240
243
  return (
241
244
  <button
242
245
  role={props.role}
@@ -247,37 +250,27 @@ function Button(this: FC, props: { role: string }) {
247
250
  Deno.exit(0); // ❌ `Deno` is unavailable in the browser
248
251
  document.title = "BOOM!"; // ✅ `document` is a browser API
249
252
  console.log(evt.target); // ✅ `evt` is the event object
250
- this.count++; // ✅ update the state `count`
253
+ this.count++; // ✅ update the `count` signal
251
254
  }}
252
255
  >
253
- Click Me
256
+ <slot />
254
257
  </button>
255
- );
258
+ )
256
259
  }
257
260
  ```
258
261
 
259
- Additionally, mono-jsx supports the `mount` event for when elements are mounted in the client-side DOM:
260
-
261
- ```jsx
262
- function App() {
263
- return (
264
- <div onMount={(evt) => console.log(evt.target, "Mounted!")}>
265
- <h1>Welcome to mono-jsx!</h1>
266
- </div>
267
- );
268
- }
269
- ```
262
+ ### Using `<form>` Element
270
263
 
271
- mono-jsx also accepts functions for the `action` property on `form` elements, which will be called on form submission:
264
+ mono-jsx supports `<form>` elements with the `action` attribute. The `action` attribute can be a string URL or a function that accepts a `FormData` object. The function will be called on form submission, and the `FormData` object will contain the form data.
272
265
 
273
266
  ```tsx
274
267
  function App() {
275
268
  return (
276
- <form action={(data: FormData, event: SubmitEvent) => console.log(data.get("name"))}>
269
+ <form action={(data: FormData) => console.log(data.get("name"))}>
277
270
  <input type="text" name="name" />
278
271
  <button type="submit">Submit</button>
279
272
  </form>
280
- );
273
+ )
281
274
  }
282
275
  ```
283
276
 
@@ -291,18 +284,18 @@ async function Loader(props: { url: string }) {
291
284
  return <JsonViewer data={data} />;
292
285
  }
293
286
 
294
- default {
287
+ export default {
295
288
  fetch: (req) => (
296
289
  <html>
297
290
  <Loader url="https://api.example.com/data" placeholder={<p>Loading...</p>} />
298
291
  </html>
299
- ),
300
- };
292
+ )
293
+ }
301
294
  ```
302
295
 
303
296
  You can also use async generators to yield multiple elements over time. This is useful for streaming rendering of LLM tokens:
304
297
 
305
- ```jsx
298
+ ```tsx
306
299
  async function* Chat(props: { prompt: string }) {
307
300
  const stream = await openai.chat.completions.create({
308
301
  model: "gpt-4",
@@ -318,113 +311,169 @@ async function* Chat(props: { prompt: string }) {
318
311
  }
319
312
  }
320
313
 
321
-
322
- default {
314
+ export default {
323
315
  fetch: (req) => (
324
316
  <html>
325
317
  <Chat prompt="Tell me a story" placeholder={<span style="color:grey">●</span>} />
326
318
  </html>
327
- ),
328
- };
319
+ )
320
+ }
329
321
  ```
330
322
 
331
- ## Reactive
323
+ ## Using Signals
332
324
 
333
- mono-jsx provides a minimal state runtime for updating the view based on client-side state changes.
325
+ mono-jsx uses signals for updating the view when a signal changes. Signals are similar to React's state, but they are more lightweight and efficient. You can use signals to manage state in your components.
334
326
 
335
- ### Using Component State
327
+ ### Using Component Signals
336
328
 
337
- You can use the `this` keyword in your components to manage state. The state is bound to the component instance and can be updated directly, and will automatically re-render the view when the state changes:
329
+ You can use the `this` keyword in your components to manage signals. The signals is bound to the component instance and can be updated directly, and will automatically re-render the view when a signal changes:
338
330
 
339
331
  ```tsx
340
332
  function Counter(
341
333
  this: FC<{ count: number }>,
342
334
  props: { initialCount?: number },
343
335
  ) {
344
- // Initialize state
336
+ // Initialize a singal
345
337
  this.count = props.initialCount ?? 0;
346
338
 
347
339
  return (
348
340
  <div>
349
- {/* render state */}
341
+ {/* render singal */}
350
342
  <span>{this.count}</span>
351
343
 
352
- {/* Update state to trigger re-render */}
344
+ {/* Update singal to trigger re-render */}
353
345
  <button onClick={() => this.count--}>-</button>
354
346
  <button onClick={() => this.count++}>+</button>
355
347
  </div>
356
- );
348
+ )
357
349
  }
358
350
  ```
359
351
 
360
- ### Using App State
352
+ ### Using App Signals
361
353
 
362
- You can define app state by adding `appState` prop to the root `<html>` element. The app state is available in all components via `this.app.<stateKey>`. Changes to the app state will trigger re-renders in all components that use it:
354
+ You can define app signals by adding `app` prop to the root `<html>` element. The app signals is available in all components via `this.app.<SignalName>`. Changes to the app signals will trigger re-renders in all components that use it:
363
355
 
364
356
  ```tsx
365
- function Header(this: FC<{}, { title: string }>) {
357
+ interface AppSignals {
358
+ themeColor: string;
359
+ }
360
+
361
+ function Header(this: FC<{}, AppSignals>) {
366
362
  return (
367
363
  <header>
368
- <h1>{this.app.title}</h1>
364
+ <h1 style={this.computed(() => ({ color: this.app.themeColor }))}>Welcome to mono-jsx!</h1>
369
365
  </header>
370
- );
366
+ )
371
367
  }
372
368
 
373
- function Footer(this: FC<{}, { title: string }>) {
369
+ function Footer(this: FC<{}, AppSignals>) {
374
370
  return (
375
371
  <footer>
376
- <p>(c) 2025 {this.app.title}</p>
372
+ <p style={this.computed(() => ({ color: this.app.themeColor }))}>(c) 2025 mono-jsx.</p>
377
373
  </footer>
378
- );
374
+ )
379
375
  }
380
376
 
381
- function Main(this: FC<{}, { title: string }>) {
377
+ function Main(this: FC<{}, AppSignals>) {
382
378
  return (
383
379
  <main>
384
- <h1>{this.app.title}</h1>
385
- <h2>Changing the title</h2>
386
- <input
387
- onInput={(evt) => this.app.title = evt.target.value}
388
- placeholder="Enter a new title"
389
- />
380
+ <p>
381
+ <label>Theme Color: </label>
382
+ <input type="color" onInput={({ target }) => this.app.themeColor = target.value}/>
383
+ </p>
390
384
  </main>
391
- );
385
+ )
392
386
  }
393
387
 
394
388
  export default {
395
389
  fetch: (req) => (
396
- <html appState={{ title: "Welcome to mono-jsx!" }}>
390
+ <html app={{ themeColor: "#232323" }}>
397
391
  <Header />
398
392
  <Main />
399
393
  <Footer />
400
394
  </html>
401
- ),
402
- };
395
+ )
396
+ }
403
397
  ```
404
398
 
405
- ### Using Computed State
399
+ ### Using Computed Signals
406
400
 
407
- You can use `this.computed` to create computed state based on state. The computed state will automatically update when the state changes:
401
+ You can use `this.computed` to create a derived signal based on other signals:
408
402
 
409
403
  ```tsx
410
404
  function App(this: FC<{ input: string }>) {
411
- this.input = "Welcome to mono-jsx!";
405
+ this.input = "Welcome to mono-jsx";
412
406
  return (
413
407
  <div>
414
408
  <h1>{this.computed(() => this.input + "!")}</h1>
415
409
 
416
410
  <form action={(fd) => this.input = fd.get("input") as string}>
417
- <input type="text" name="input" value={this.input} />
411
+ <input type="text" name="input" value={"" + this.input} />
418
412
  <button type="submit">Submit</button>
419
413
  </form>
420
414
  </div>
421
- );
415
+ )
416
+ }
417
+ ```
418
+
419
+ ### Using Effects
420
+
421
+ You can use `this.effect` to create side effects based on signals. The effect will run whenever the signal changes:
422
+
423
+ ```tsx
424
+ function App(this: FC<{ count: number }>) {
425
+ this.count = 0;
426
+
427
+ this.effect(() => {
428
+ console.log("Count changed:", this.count);
429
+ });
430
+
431
+ return (
432
+ <div>
433
+ <span>{this.count}</span>
434
+ <button onClick={() => this.count++}>+</button>
435
+ </div>
436
+ )
437
+ }
438
+ ```
439
+
440
+ The callback function of `this.effect` can return a cleanup function that gets run once the component element has been removed via `<toggle>` or `<switch>` condition rendering:
441
+
442
+ ```tsx
443
+ function Counter(this: FC<{ count: number }>) {
444
+ this.count = 0;
445
+
446
+ this.effect(() => {
447
+ const interval = setInterval(() => {
448
+ this.count++;
449
+ }, 1000);
450
+
451
+ return () => clearInterval(interval);
452
+ });
453
+
454
+ return (
455
+ <div>
456
+ <span>{this.count}</span>
457
+ </div>
458
+ )
459
+ }
460
+
461
+ function App(this: FC<{ show: boolean }>) {
462
+ this.show = true
463
+ return (
464
+ <div>
465
+ <toggle show={this.show}>
466
+ <Foo />
467
+ </toggle>
468
+ <button onClick={e => this.show = !this.show }>{this.computed(() => this.show ? 'Hide': 'Show')}</button>
469
+ </div>
470
+ )
422
471
  }
423
472
  ```
424
473
 
425
- ### Using `<toggle>` Element with State
474
+ ### Using `<toggle>` Element with Signals
426
475
 
427
- The `<toggle>` element conditionally renders content based on the value of a state.
476
+ The `<toggle>` element conditionally renders content based on the `show` prop:
428
477
 
429
478
  ```tsx
430
479
  function App(this: FC<{ show: boolean }>) {
@@ -436,7 +485,7 @@ function App(this: FC<{ show: boolean }>) {
436
485
 
437
486
  return (
438
487
  <div>
439
- <toggle value={this.show}>
488
+ <toggle show={this.show}>
440
489
  <h1>Welcome to mono-jsx!</h1>
441
490
  </toggle>
442
491
 
@@ -444,16 +493,16 @@ function App(this: FC<{ show: boolean }>) {
444
493
  {this.computed(() => this.show ? "Hide" : "Show")}
445
494
  </button>
446
495
  </div>
447
- );
496
+ )
448
497
  }
449
498
  ```
450
499
 
451
- ### Using `<switch>` Element with State
500
+ ### Using `<switch>` Element with Signals
452
501
 
453
- The `<switch>` element renders different content based on the value of a state. Elements with matching `slot` attributes are displayed when their value matches, otherwise default content is shown:
502
+ The `<switch>` element renders different content based on the value of a signal. Elements with matching `slot` attributes are displayed when their value matches, otherwise default slots are shown:
454
503
 
455
504
  ```tsx
456
- function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
505
+ function App(this: FC<{ lang: "en" | "zh" | "🙂" }>) {
457
506
  this.lang = "en";
458
507
 
459
508
  return (
@@ -466,19 +515,19 @@ function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
466
515
  <p>
467
516
  <button onClick={() => this.lang = "en"}>English</button>
468
517
  <button onClick={() => this.lang = "zh"}>中文</button>
469
- <button onClick={() => this.lang = "emoji"}>Emoji</button>
518
+ <button onClick={() => this.lang = "🙂"}>🙂</button>
470
519
  </p>
471
520
  </div>
472
- );
521
+ )
473
522
  }
474
523
  ```
475
524
 
476
- ### Limitation of State
525
+ ### Limitation of Signals
477
526
 
478
- 1\. Component state cannot be used in arrow function components.
527
+ 1\. Arrow function are non-stateful components.
479
528
 
480
529
  ```tsx
481
- // ❌ Won't work - state updates won't refresh the view
530
+ // ❌ Won't work - use `this` in a non-stateful component
482
531
  const App = () => {
483
532
  this.count = 0;
484
533
  return (
@@ -486,7 +535,7 @@ const App = () => {
486
535
  <span>{this.count}</span>
487
536
  <button onClick={() => this.count++}>+</button>
488
537
  </div>
489
- );
538
+ )
490
539
  };
491
540
 
492
541
  // ✅ Works correctly
@@ -497,16 +546,16 @@ function App(this: FC) {
497
546
  <span>{this.count}</span>
498
547
  <button onClick={() => this.count++}>+</button>
499
548
  </div>
500
- );
549
+ )
501
550
  }
502
551
  ```
503
552
 
504
- 2\. Component state cannot be computed outside of the `this.computed` method.
553
+ 2\. Signals cannot be computed outside of the `this.computed` method.
505
554
 
506
555
  ```tsx
507
- // ❌ Won't work - state updates won't refresh the view
556
+ // ❌ Won't work - updates of a signal won't refresh the view
508
557
  function App(this: FC<{ message: string }>) {
509
- this.message = "Welcome to mono-jsx!";
558
+ this.message = "Welcome to mono-jsx";
510
559
  return (
511
560
  <div>
512
561
  <h1 title={this.message + "!"}>{this.message + "!"}</h1>
@@ -514,12 +563,12 @@ function App(this: FC<{ message: string }>) {
514
563
  Click Me
515
564
  </button>
516
565
  </div>
517
- );
566
+ )
518
567
  }
519
568
 
520
569
  // ✅ Works correctly
521
570
  function App(this: FC) {
522
- this.message = "Welcome to mono-jsx!";
571
+ this.message = "Welcome to mono-jsx";
523
572
  return (
524
573
  <div>
525
574
  <h1 title={this.computed(() => this.message + "!")}>{this.computed(() => this.message + "!")}</h1>
@@ -527,33 +576,89 @@ function App(this: FC) {
527
576
  Click Me
528
577
  </button>
529
578
  </div>
530
- );
579
+ )
580
+ }
581
+ ```
582
+
583
+ 3\. The callback function of `this.computed` must be a pure function. That means it should not create side effects or access any non-stateful variables. For example, you cannot use `Deno` or `document` in the callback function:
584
+
585
+ ```tsx
586
+ // ❌ Won't work - throws `Deno is not defined` when the button is clicked
587
+ function App(this: FC<{ message: string }>) {
588
+ this.message = "Welcome to mono-jsx";
589
+ return (
590
+ <div>
591
+ <h1>{this.computed(() => this.message + "! (Deno " + Deno.version.deno + ")")}</h1>
592
+ <button onClick={() => this.message = "Clicked"}>
593
+ Click Me
594
+ </button>
595
+ </div>
596
+ )
597
+ }
598
+
599
+ // ✅ Works correctly
600
+ function App(this: FC<{ message: string, denoVersion: string }>) {
601
+ this.denoVersion = Deno.version.deno;
602
+ this.message = "Welcome to mono-jsx";
603
+ return (
604
+ <div>
605
+ <h1>{this.computed(() => this.message + "! (Deno " + this.denoVersion + ")")}</h1>
606
+ <button onClick={() => this.message = "Clicked"}>
607
+ Click Me
608
+ </button>
609
+ </div>
610
+ )
531
611
  }
532
612
  ```
533
613
 
534
614
  ## Using `this` in Components
535
615
 
536
- mono-jsx binds a special `this` object to your components when they are rendered. This object contains properties and methods that you can use to manage state, context, and other features.
616
+ mono-jsx binds a scoped signals object to `this` of your component functions. This allows you to access signals, context, and request information directly in your components.
537
617
 
538
- The `this` object contains the following properties:
618
+ The `this` object has the following built-in properties:
539
619
 
540
- - `app`: The app state defined on the root `<html>` element.
620
+ - `app`: The app signals defined on the root `<html>` element.
541
621
  - `context`: The context defined on the root `<html>` element.
542
622
  - `request`: The request object from the `fetch` handler.
543
- - `computed`: A method to create computed properties based on state.
623
+ - `refs`: A map of refs defined in the component.
624
+ - `computed`: A method to create a computed signal.
625
+ - `effect`: A method to create side effects.
544
626
 
545
627
  ```ts
546
- type FC<State = {}, AppState = {}, Context = {}> = {
547
- readonly app: AppState;
628
+ type FC<Signals = {}, AppSignals = {}, Context = {}> = {
629
+ readonly app: AppSignals;
548
630
  readonly context: Context;
549
- readonly request: Request;
550
- readonly computed: <V = unknown>(computeFn: () => V) => V;
551
- } & Omit<State, "app" | "context" | "request" | "computed">;
631
+ readonly request: Request & { params?: Record<string, string> };
632
+ readonly refs: Record<string, HTMLElement | null>;
633
+ readonly computed: <T = unknown>(fn: () => T) => T;
634
+ readonly effect: (fn: () => void | (() => void)) => void;
635
+ } & Omit<Signals, "app" | "context" | "request" | "computed" | "effect">;
552
636
  ```
553
637
 
554
- ### Using State
638
+ ### Using Signals
639
+
640
+ See the [Using Signals](#using-signals) section for more details on how to use signals in your components.
555
641
 
556
- Check the [Using State](#using-state) section for more details on how to use state in your components.
642
+ ### Using Refs
643
+
644
+ You can use `this.refs` to access refs in your components. Define refs in your component using the `ref` attribute:
645
+
646
+ ```tsx
647
+ function App(this: FC) {
648
+ this.effect(() => {
649
+ this.refs.input?.addEventListener("input", (evt) => {
650
+ console.log("Input changed:", evt.target.value);
651
+ });
652
+ });
653
+
654
+ return (
655
+ <div>
656
+ <input ref={this.refs.input} type="text" />
657
+ <button onClick={() => this.refs.input?.focus()}>Focus</button>
658
+ </div>
659
+ )
660
+ }
661
+ ```
557
662
 
558
663
  ### Using Context
559
664
 
@@ -567,7 +672,7 @@ function Dash(this: FC<{}, {}, { auth: { uuid: string; name: string } }>) {
567
672
  <h1>Welcome back, {auth.name}!</h1>
568
673
  <p>Your UUID is {auth.uuid}</p>
569
674
  </div>
570
- );
675
+ )
571
676
  }
572
677
 
573
678
  export default {
@@ -578,9 +683,9 @@ export default {
578
683
  {!auth && <p>Please Login</p>}
579
684
  {auth && <Dash />}
580
685
  </html>
581
- );
582
- },
583
- };
686
+ )
687
+ }
688
+ }
584
689
  ```
585
690
 
586
691
  ### Accessing Request Info
@@ -597,7 +702,7 @@ function RequestInfo(this: FC) {
597
702
  <p>{request.url}</p>
598
703
  <p>{request.headers.get("user-agent")}</p>
599
704
  </div>
600
- );
705
+ )
601
706
  }
602
707
 
603
708
  export default {
@@ -605,15 +710,15 @@ export default {
605
710
  <html request={req}>
606
711
  <RequestInfo />
607
712
  </html>
608
- ),
609
- };
713
+ )
714
+ }
610
715
  ```
611
716
 
612
717
  ## Streaming Rendering
613
718
 
614
719
  mono-jsx renders your `<html>` as a readable stream, allowing async components to render asynchronously. You can use `placeholder` to display a loading state while waiting for async components to render:
615
720
 
616
- ```jsx
721
+ ```tsx
617
722
  async function Sleep({ ms }) {
618
723
  await new Promise((resolve) => setTimeout(resolve, ms));
619
724
  return <slot />;
@@ -626,13 +731,13 @@ export default {
626
731
  <p>After 1 second</p>
627
732
  </Sleep>
628
733
  </html>
629
- ),
630
- };
734
+ )
735
+ }
631
736
  ```
632
737
 
633
738
  You can set the `rendering` attribute to `"eager"` to force synchronous rendering (the `placeholder` will be ignored):
634
739
 
635
- ```jsx
740
+ ```tsx
636
741
  export default {
637
742
  fetch: (req) => (
638
743
  <html>
@@ -640,13 +745,13 @@ export default {
640
745
  <p>After 1 second</p>
641
746
  </Sleep>
642
747
  </html>
643
- ),
644
- };
748
+ )
749
+ }
645
750
  ```
646
751
 
647
752
  You can add the `catch` attribute to handle errors in the async component. The `catch` attribute should be a function that returns a JSX element:
648
753
 
649
- ```jsx
754
+ ```tsx
650
755
  async function Hello() {
651
756
  throw new Error("Something went wrong!");
652
757
  return <p>Hello world!</p>;
@@ -657,15 +762,212 @@ export default {
657
762
  <html>
658
763
  <Hello catch={err => <p>{err.message}</p>} />
659
764
  </html>
660
- ),
661
- };
765
+ )
766
+ }
767
+ ```
768
+
769
+
770
+ ## Lazy Rendering
771
+
772
+ Since mono-jsx renders html on server side, and no hydration JS sent to client side. To render a component dynamically on client side, you can use the `<component>` element to ask the server to render a component and send the html back to client:
773
+
774
+ ```tsx
775
+ export default {
776
+ fetch: (req) => (
777
+ <html components={{ Foo }}>
778
+ <component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
779
+ </html>
780
+ )
781
+ }
782
+ ```
783
+
784
+ You can use `<toggle>` element to control when to render the component:
785
+
786
+ ```tsx
787
+ async function Lazy(this: FC<{ show: boolean }>, props: { url: string }) {
788
+ this.show = false;
789
+ return (
790
+ <div>
791
+ <toggle value={this.show}>
792
+ <component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
793
+ </toggle>
794
+ <button onClick={() => this.show = true }>Load `Foo` Component</button>
795
+ </div>
796
+ )
797
+ }
798
+
799
+ export default {
800
+ fetch: (req) => (
801
+ <html components={{ Foo }}>
802
+ <Lazy />
803
+ </html>
804
+ )
805
+ }
806
+ ```
807
+
808
+ You also can use signal `name` or `props`, change the signal value will trigger the component to re-render with new name or props:
809
+
810
+ ```tsx
811
+ import { Profile, Projects, Settings } from "./pages.tsx"
812
+
813
+ async function Dash(this: FC<{ page: "Profile" | "Projects" | "Settings" }>) {
814
+ this.page = "Projects";
815
+
816
+ return (
817
+ <>
818
+ <div class="tab">
819
+ <button onClick={e => this.page = "Profile"}>Profile</botton>
820
+ <button onClick={e => this.page = "Projects"}>Projects</botton>
821
+ <button onClick={e => this.page = "Settings"}>Settings</botton>
822
+ </div>
823
+ <div class="page">
824
+ <component name={this.page} placeholder={<p>Loading...</p>} />
825
+ </div>
826
+ </>
827
+ )
828
+ }
829
+
830
+ export default {
831
+ fetch: (req) => (
832
+ <html components={{ Profile, Projects, Settings }}>
833
+ <Dash />
834
+ </html>
835
+ )
836
+ }
837
+ ```
838
+
839
+ ## Using Router(SPA)
840
+
841
+ mono-jsx provides a built-in `<router>` element that allows your app to render components based on the current URL. On client side, it hijacks all `click` events on `<a>` elements and asynchronously fetches the route component without reloading the entire page.
842
+
843
+ To use the router, you need to define your routes as a mapping of URL patterns to components and pass it to the `<html>` element as `routes` prop. The `request` prop is also required to match the current URL against the defined routes.
844
+
845
+ ```tsx
846
+ const routes = {
847
+ "/": Home,
848
+ "/about": About,
849
+ "/blog": Blog,
850
+ "/post/:id": Post,
851
+ }
852
+
853
+ export default {
854
+ fetch: (req) => (
855
+ <html request={req} routes={routes}>
856
+ <header>
857
+ <nav>
858
+ <a href="/">Home</a>
859
+ <a href="/about">About</a>
860
+ <a href="/blog">Blog</a>
861
+ </nav>
862
+ </header>
863
+ <router />
864
+ </html>
865
+ )
866
+ }
867
+ ```
868
+
869
+ mono-jsx router requires [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) to match a route:
870
+
871
+ - ✅ Deno
872
+ - ✅ Cloudflare Workers
873
+ - ✅ Nodejs (>= 24)
874
+
875
+ For Bun users, mono-jsx provides a `monoRoutes` function that uses Bun's builtin routing:
876
+
877
+ ```tsx
878
+ // bun app.tsx
879
+
880
+ import { monoRoutes } from "mono-jsx"
881
+
882
+ const routes = {
883
+ "/": Home,
884
+ "/about": About,
885
+ "/blog": Blog,
886
+ "/post/:id": Post,
887
+ }
888
+
889
+ export default {
890
+ routes: monoRoutes(routes, (request) => (
891
+ <html request={request}>
892
+ <router />
893
+ </html>
894
+ ))
895
+ }
896
+ ```
897
+
898
+ ### Using Route `params`
899
+
900
+ When you define a route with a parameter (e.g., `/post/:id`), mono-jsx will automatically extract the parameter from the URL and make it available in the route component. The `params` object is available in the `request` property of the component's `this` context.
901
+ You can access the `params` object in your route components to get the values of the parameters defined in the route pattern:
902
+
903
+
904
+ ```tsx
905
+ // router pattern: "/post/:id"
906
+ function Post(this: FC) {
907
+ this.request.url // "http://localhost:3000/post/123"
908
+ this.request.params?.id // "123"
909
+ }
910
+ ```
911
+
912
+ ### Using DB/Storage in Route Components
913
+
914
+ Route components are always rendered on server-side, you can use any database or storage API to fetch data in your route components. For example, if you're using a SQL database, you can use the `sql` tag function to query the database:
915
+
916
+ ```tsx
917
+ async function Post(this: FC) {
918
+ const post = await sql`SELECT * FROM posts WHERE id = ${ this.request.params!.id }`
919
+ return (
920
+ <article>
921
+ <h2>{post.title}<h2>
922
+ <div>html`${post.content}`</div>
923
+ </article>
924
+ )
925
+ }
926
+ ```
927
+
928
+ ### Nav Links
929
+
930
+ Links under a `<nav>` element will be treated as navigation links by the router. When the `href` of a link matches a route, A active class will be added to the link element. By default, the active class is `active`, but you can customize it by setting the `data-active-class` attribute on the `<nav>` element. You can also add a custom style for the active link using nested CSS selectors in the `style` attribute of the `<nav>` element:
931
+
932
+ ```tsx
933
+ export default {
934
+ fetch: (req) => (
935
+ <html request={req} routes={routes}>
936
+ <header>
937
+ <nav style={{ "& a.active": { fontWeight: "bold" } }} data-active-class="active">
938
+ <a href="/">Home</a>
939
+ <a href="/about">About</a>
940
+ <a href="/blog">Blog</a>
941
+ </nav>
942
+ </header>
943
+ <router />
944
+ </html>
945
+ )
946
+ }
947
+ ```
948
+
949
+ ### Setting Fallback(404) Content
950
+
951
+ You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL:
952
+
953
+ ```tsx
954
+ export default {
955
+ fetch: (req) => (
956
+ <html request={req} routes={routes}>
957
+ <router>
958
+ <p>Page Not Found</p>
959
+ <p>Back to <a href="/">Home</a></p>
960
+ </router>
961
+ </html>
962
+ )
963
+ }
662
964
  ```
663
965
 
664
966
  ## Customizing html Response
665
967
 
666
968
  You can add `status` or `headers` attributes to the root `<html>` element to customize the http response:
667
969
 
668
- ```jsx
970
+ ```tsx
669
971
  export default {
670
972
  fetch: (req) => (
671
973
  <html
@@ -678,15 +980,15 @@ export default {
678
980
  >
679
981
  <h1>Page Not Found</h1>
680
982
  </html>
681
- ),
682
- };
983
+ )
984
+ }
683
985
  ```
684
986
 
685
987
  ### Using htmx
686
988
 
687
989
  mono-jsx integrates with [htmx](https://htmx.org/) and [typed-htmx](https://github.com/Desdaemon/typed-htmx). To use htmx, add the `htmx` attribute to the root `<html>` element:
688
990
 
689
- ```jsx
991
+ ```tsx
690
992
  export default {
691
993
  fetch: (req) => {
692
994
  const url = new URL(req.url);
@@ -705,16 +1007,16 @@ export default {
705
1007
  Click Me
706
1008
  </button>
707
1009
  </html>
708
- );
709
- },
710
- };
1010
+ )
1011
+ }
1012
+ }
711
1013
  ```
712
1014
 
713
1015
  #### Adding htmx Extensions
714
1016
 
715
1017
  You can add htmx [extensions](https://htmx.org/docs/#extensions) by adding the `htmx-ext-*` attribute to the root `<html>` element:
716
1018
 
717
- ```jsx
1019
+ ```tsx
718
1020
  export default {
719
1021
  fetch: (req) => (
720
1022
  <html htmx htmx-ext-response-targets htmx-ext-ws>
@@ -722,15 +1024,15 @@ export default {
722
1024
  Click Me
723
1025
  </button>
724
1026
  </html>
725
- ),
726
- };
1027
+ )
1028
+ }
727
1029
  ```
728
1030
 
729
1031
  #### Specifying htmx Version
730
1032
 
731
1033
  You can specify the htmx version by setting the `htmx` attribute to a specific version:
732
1034
 
733
- ```jsx
1035
+ ```tsx
734
1036
  export default {
735
1037
  fetch: (req) => (
736
1038
  <html htmx="2.0.4" htmx-ext-response-targets="2.0.2" htmx-ext-ws="2.0.2">
@@ -738,15 +1040,15 @@ export default {
738
1040
  Click Me
739
1041
  </button>
740
1042
  </html>
741
- ),
742
- };
1043
+ )
1044
+ }
743
1045
  ```
744
1046
 
745
1047
  #### Installing htmx Manually
746
1048
 
747
1049
  By default, mono-jsx installs htmx from [esm.sh](https://esm.sh/) CDN when you set the `htmx` attribute. You can also install htmx manually with your own CDN or local copy:
748
1050
 
749
- ```jsx
1051
+ ```tsx
750
1052
  export default {
751
1053
  fetch: (req) => (
752
1054
  <html>
@@ -760,8 +1062,8 @@ export default {
760
1062
  </button>
761
1063
  </body>
762
1064
  </html>
763
- ),
764
- };
1065
+ )
1066
+ }
765
1067
  ```
766
1068
 
767
1069
  ## License