mono-jsx 0.4.0 → 0.5.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
@@ -6,8 +6,8 @@ mono-jsx is a JSX runtime that renders `<html>` element to `Response` object in
6
6
 
7
7
  - 🚀 No build step needed
8
8
  - 🦋 Lightweight (8KB gzipped), zero dependencies
9
- - 🔫 Minimal state runtime
10
- - 🚨 Complete Web API TypeScript definitions
9
+ - 🚦 Signals as reactive primitives
10
+ - 🗂️ Complete Web API TypeScript definitions
11
11
  - ⏳ Streaming rendering
12
12
  - 🥷 [htmx](#using-htmx) integration
13
13
  - 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
@@ -72,8 +72,8 @@ export default {
72
72
  <html>
73
73
  <h1>Welcome to mono-jsx!</h1>
74
74
  </html>
75
- ),
76
- };
75
+ )
76
+ }
77
77
  ```
78
78
 
79
79
  For Deno/Bun users, you can run the `app.tsx` directly:
@@ -173,7 +173,7 @@ function Container() {
173
173
  {/* Named slot */}
174
174
  <slot name="desc" />
175
175
  </div>
176
- );
176
+ )
177
177
  }
178
178
 
179
179
  function App() {
@@ -184,7 +184,7 @@ function App() {
184
184
  {/* This goes to the default slot */}
185
185
  <h1>Hello world!</h1>
186
186
  </Container>
187
- );
187
+ )
188
188
  }
189
189
  ```
190
190
 
@@ -207,7 +207,7 @@ function App() {
207
207
  <style>{css`h1 { font-size: 3rem; }`}</style>
208
208
  <script>{js`console.log("Hello world!")`}</script>
209
209
  </head>
210
- );
210
+ )
211
211
  }
212
212
  ```
213
213
 
@@ -224,7 +224,7 @@ function Button() {
224
224
  <button onClick={(evt) => alert("BOOM!")}>
225
225
  Click Me
226
226
  </button>
227
- );
227
+ )
228
228
  }
229
229
  ```
230
230
 
@@ -234,9 +234,10 @@ function Button() {
234
234
  ```tsx
235
235
  import { doSomething } from "some-library";
236
236
 
237
- function Button(this: FC, props: { role: string }) {
238
- let message = "BOOM!";
239
- console.log(message); // only executes on server-side
237
+ function Button(this: FC<{ count: 0 }>, props: { role: string }) {
238
+ const message = "BOOM!"; // server-side variable
239
+ this.count = 0; // initialize a signal
240
+ console.log(message); // only prints on server-side
240
241
  return (
241
242
  <button
242
243
  role={props.role}
@@ -247,37 +248,27 @@ function Button(this: FC, props: { role: string }) {
247
248
  Deno.exit(0); // ❌ `Deno` is unavailable in the browser
248
249
  document.title = "BOOM!"; // ✅ `document` is a browser API
249
250
  console.log(evt.target); // ✅ `evt` is the event object
250
- this.count++; // ✅ update the state `count`
251
+ this.count++; // ✅ update the `count` signal
251
252
  }}
252
253
  >
253
- Click Me
254
+ <slot />
254
255
  </button>
255
- );
256
+ )
256
257
  }
257
258
  ```
258
259
 
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
- ```
260
+ ### Using `<form>` Element
270
261
 
271
- mono-jsx also accepts functions for the `action` property on `form` elements, which will be called on form submission:
262
+ 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
263
 
273
264
  ```tsx
274
265
  function App() {
275
266
  return (
276
- <form action={(data: FormData, event: SubmitEvent) => console.log(data.get("name"))}>
267
+ <form action={(data: FormData) => console.log(data.get("name"))}>
277
268
  <input type="text" name="name" />
278
269
  <button type="submit">Submit</button>
279
270
  </form>
280
- );
271
+ )
281
272
  }
282
273
  ```
283
274
 
@@ -291,13 +282,13 @@ async function Loader(props: { url: string }) {
291
282
  return <JsonViewer data={data} />;
292
283
  }
293
284
 
294
- default {
285
+ export default {
295
286
  fetch: (req) => (
296
287
  <html>
297
288
  <Loader url="https://api.example.com/data" placeholder={<p>Loading...</p>} />
298
289
  </html>
299
- ),
300
- };
290
+ )
291
+ }
301
292
  ```
302
293
 
303
294
  You can also use async generators to yield multiple elements over time. This is useful for streaming rendering of LLM tokens:
@@ -318,113 +309,169 @@ async function* Chat(props: { prompt: string }) {
318
309
  }
319
310
  }
320
311
 
321
-
322
- default {
312
+ export default {
323
313
  fetch: (req) => (
324
314
  <html>
325
315
  <Chat prompt="Tell me a story" placeholder={<span style="color:grey">●</span>} />
326
316
  </html>
327
- ),
328
- };
317
+ )
318
+ }
329
319
  ```
330
320
 
331
- ## Reactive
321
+ ## Using Signals
332
322
 
333
- mono-jsx provides a minimal state runtime for updating the view based on client-side state changes.
323
+ 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
324
 
335
- ### Using Component State
325
+ ### Using Component Signals
336
326
 
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:
327
+ 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
328
 
339
329
  ```tsx
340
330
  function Counter(
341
331
  this: FC<{ count: number }>,
342
332
  props: { initialCount?: number },
343
333
  ) {
344
- // Initialize state
334
+ // Initialize a singal
345
335
  this.count = props.initialCount ?? 0;
346
336
 
347
337
  return (
348
338
  <div>
349
- {/* render state */}
339
+ {/* render singal */}
350
340
  <span>{this.count}</span>
351
341
 
352
- {/* Update state to trigger re-render */}
342
+ {/* Update singal to trigger re-render */}
353
343
  <button onClick={() => this.count--}>-</button>
354
344
  <button onClick={() => this.count++}>+</button>
355
345
  </div>
356
- );
346
+ )
357
347
  }
358
348
  ```
359
349
 
360
- ### Using App State
350
+ ### Using App Signals
361
351
 
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:
352
+ 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
353
 
364
354
  ```tsx
365
- function Header(this: FC<{}, { title: string }>) {
355
+ interface AppSignals {
356
+ themeColor: string;
357
+ }
358
+
359
+ function Header(this: FC<{}, AppSignals>) {
366
360
  return (
367
361
  <header>
368
- <h1>{this.app.title}</h1>
362
+ <h1 style={this.computed(() => ({ color: this.app.themeColor }))}>Welcome to mono-jsx!</h1>
369
363
  </header>
370
- );
364
+ )
371
365
  }
372
366
 
373
- function Footer(this: FC<{}, { title: string }>) {
367
+ function Footer(this: FC<{}, AppSignals>) {
374
368
  return (
375
369
  <footer>
376
- <p>(c) 2025 {this.app.title}</p>
370
+ <p style={this.computed(() => ({ color: this.app.themeColor }))}>(c) 2025 mono-jsx.</p>
377
371
  </footer>
378
- );
372
+ )
379
373
  }
380
374
 
381
- function Main(this: FC<{}, { title: string }>) {
375
+ function Main(this: FC<{}, AppSignals>) {
382
376
  return (
383
377
  <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
- />
378
+ <p>
379
+ <label>Theme Color: </label>
380
+ <input type="color" onInput={({ target }) => this.app.themeColor = target.value}/>
381
+ </p>
390
382
  </main>
391
- );
383
+ )
392
384
  }
393
385
 
394
386
  export default {
395
387
  fetch: (req) => (
396
- <html appState={{ title: "Welcome to mono-jsx!" }}>
388
+ <html app={{ themeColor: "#232323" }}>
397
389
  <Header />
398
390
  <Main />
399
391
  <Footer />
400
392
  </html>
401
- ),
402
- };
393
+ )
394
+ }
403
395
  ```
404
396
 
405
- ### Using Computed State
397
+ ### Using Computed Signals
406
398
 
407
- You can use `this.computed` to create computed state based on state. The computed state will automatically update when the state changes:
399
+ You can use `this.computed` to create a derived signal based on other signals:
408
400
 
409
401
  ```tsx
410
402
  function App(this: FC<{ input: string }>) {
411
- this.input = "Welcome to mono-jsx!";
403
+ this.input = "Welcome to mono-jsx";
412
404
  return (
413
405
  <div>
414
406
  <h1>{this.computed(() => this.input + "!")}</h1>
415
407
 
416
408
  <form action={(fd) => this.input = fd.get("input") as string}>
417
- <input type="text" name="input" value={this.input} />
409
+ <input type="text" name="input" value={"" + this.input} />
418
410
  <button type="submit">Submit</button>
419
411
  </form>
420
412
  </div>
421
- );
413
+ )
414
+ }
415
+ ```
416
+
417
+ ### Using Effects
418
+
419
+ You can use `this.effect` to create side effects based on signals. The effect will run whenever the signal changes:
420
+
421
+ ```tsx
422
+ function App(this: FC<{ count: number }>) {
423
+ this.count = 0;
424
+
425
+ this.effect(() => {
426
+ console.log("Count changed:", this.count);
427
+ });
428
+
429
+ return (
430
+ <div>
431
+ <span>{this.count}</span>
432
+ <button onClick={() => this.count++}>+</button>
433
+ </div>
434
+ )
435
+ }
436
+ ```
437
+
438
+ 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:
439
+
440
+ ```tsx
441
+ function Counter(this: FC<{ count: number }>) {
442
+ this.count = 0;
443
+
444
+ this.effect(() => {
445
+ const interval = setInterval(() => {
446
+ this.count++;
447
+ }, 1000);
448
+
449
+ return () => clearInterval(interval);
450
+ });
451
+
452
+ return (
453
+ <div>
454
+ <span>{this.count}</span>
455
+ </div>
456
+ )
457
+ }
458
+
459
+ function App(this: FC<{ show: boolean }>) {
460
+ this.show = true
461
+ return (
462
+ <div>
463
+ <toggle show={this.show}>
464
+ <Foo />
465
+ </toggle>
466
+ <button onClick={e => this.show = !this.show }>{this.computed(() => this.show ? 'Hide': 'Show')}</button>
467
+ </div>
468
+ )
422
469
  }
423
470
  ```
424
471
 
425
- ### Using `<toggle>` Element with State
472
+ ### Using `<toggle>` Element with Signals
426
473
 
427
- The `<toggle>` element conditionally renders content based on the value of a state.
474
+ The `<toggle>` element conditionally renders content based on the `show` prop:
428
475
 
429
476
  ```tsx
430
477
  function App(this: FC<{ show: boolean }>) {
@@ -436,7 +483,7 @@ function App(this: FC<{ show: boolean }>) {
436
483
 
437
484
  return (
438
485
  <div>
439
- <toggle value={this.show}>
486
+ <toggle show={this.show}>
440
487
  <h1>Welcome to mono-jsx!</h1>
441
488
  </toggle>
442
489
 
@@ -444,16 +491,16 @@ function App(this: FC<{ show: boolean }>) {
444
491
  {this.computed(() => this.show ? "Hide" : "Show")}
445
492
  </button>
446
493
  </div>
447
- );
494
+ )
448
495
  }
449
496
  ```
450
497
 
451
- ### Using `<switch>` Element with State
498
+ ### Using `<switch>` Element with Signals
452
499
 
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:
500
+ 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
501
 
455
502
  ```tsx
456
- function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
503
+ function App(this: FC<{ lang: "en" | "zh" | "🙂" }>) {
457
504
  this.lang = "en";
458
505
 
459
506
  return (
@@ -466,19 +513,19 @@ function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
466
513
  <p>
467
514
  <button onClick={() => this.lang = "en"}>English</button>
468
515
  <button onClick={() => this.lang = "zh"}>中文</button>
469
- <button onClick={() => this.lang = "emoji"}>Emoji</button>
516
+ <button onClick={() => this.lang = "🙂"}>🙂</button>
470
517
  </p>
471
518
  </div>
472
- );
519
+ )
473
520
  }
474
521
  ```
475
522
 
476
- ### Limitation of State
523
+ ### Limitation of Signals
477
524
 
478
- 1\. Component state cannot be used in arrow function components.
525
+ 1\. Arrow function are non-stateful components.
479
526
 
480
527
  ```tsx
481
- // ❌ Won't work - state updates won't refresh the view
528
+ // ❌ Won't work - use `this` in a non-stateful component
482
529
  const App = () => {
483
530
  this.count = 0;
484
531
  return (
@@ -486,7 +533,7 @@ const App = () => {
486
533
  <span>{this.count}</span>
487
534
  <button onClick={() => this.count++}>+</button>
488
535
  </div>
489
- );
536
+ )
490
537
  };
491
538
 
492
539
  // ✅ Works correctly
@@ -497,16 +544,16 @@ function App(this: FC) {
497
544
  <span>{this.count}</span>
498
545
  <button onClick={() => this.count++}>+</button>
499
546
  </div>
500
- );
547
+ )
501
548
  }
502
549
  ```
503
550
 
504
- 2\. Component state cannot be computed outside of the `this.computed` method.
551
+ 2\. Signals cannot be computed outside of the `this.computed` method.
505
552
 
506
553
  ```tsx
507
- // ❌ Won't work - state updates won't refresh the view
554
+ // ❌ Won't work - updates of a signal won't refresh the view
508
555
  function App(this: FC<{ message: string }>) {
509
- this.message = "Welcome to mono-jsx!";
556
+ this.message = "Welcome to mono-jsx";
510
557
  return (
511
558
  <div>
512
559
  <h1 title={this.message + "!"}>{this.message + "!"}</h1>
@@ -514,12 +561,12 @@ function App(this: FC<{ message: string }>) {
514
561
  Click Me
515
562
  </button>
516
563
  </div>
517
- );
564
+ )
518
565
  }
519
566
 
520
567
  // ✅ Works correctly
521
568
  function App(this: FC) {
522
- this.message = "Welcome to mono-jsx!";
569
+ this.message = "Welcome to mono-jsx";
523
570
  return (
524
571
  <div>
525
572
  <h1 title={this.computed(() => this.message + "!")}>{this.computed(() => this.message + "!")}</h1>
@@ -527,33 +574,89 @@ function App(this: FC) {
527
574
  Click Me
528
575
  </button>
529
576
  </div>
530
- );
577
+ )
578
+ }
579
+ ```
580
+
581
+ 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:
582
+
583
+ ```tsx
584
+ // ❌ Won't work - throws `Deno is not defined` when the button is clicked
585
+ function App(this: FC<{ message: string }>) {
586
+ this.message = "Welcome to mono-jsx";
587
+ return (
588
+ <div>
589
+ <h1>{this.computed(() => this.message + "! (Deno " + Deno.version.deno + ")")}</h1>
590
+ <button onClick={() => this.message = "Clicked"}>
591
+ Click Me
592
+ </button>
593
+ </div>
594
+ )
595
+ }
596
+
597
+ // ✅ Works correctly
598
+ function App(this: FC<{ message: string, denoVersion: string }>) {
599
+ this.denoVersion = Deno.version.deno;
600
+ this.message = "Welcome to mono-jsx";
601
+ return (
602
+ <div>
603
+ <h1>{this.computed(() => this.message + "! (Deno " + this.denoVersion + ")")}</h1>
604
+ <button onClick={() => this.message = "Clicked"}>
605
+ Click Me
606
+ </button>
607
+ </div>
608
+ )
531
609
  }
532
610
  ```
533
611
 
534
612
  ## Using `this` in Components
535
613
 
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.
614
+ 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
615
 
538
- The `this` object contains the following properties:
616
+ The `this` object has the following built-in properties:
539
617
 
540
- - `app`: The app state defined on the root `<html>` element.
618
+ - `app`: The app signals defined on the root `<html>` element.
541
619
  - `context`: The context defined on the root `<html>` element.
542
620
  - `request`: The request object from the `fetch` handler.
543
- - `computed`: A method to create computed properties based on state.
621
+ - `refs`: A map of refs defined in the component.
622
+ - `computed`: A method to create a computed signal.
623
+ - `effect`: A method to create side effects.
544
624
 
545
625
  ```ts
546
- type FC<State = {}, AppState = {}, Context = {}> = {
547
- readonly app: AppState;
626
+ type FC<Signals = {}, AppSignals = {}, Context = {}> = {
627
+ readonly app: AppSignals;
548
628
  readonly context: Context;
549
629
  readonly request: Request;
550
- readonly computed: <V = unknown>(computeFn: () => V) => V;
551
- } & Omit<State, "app" | "context" | "request" | "computed">;
630
+ readonly refs: Record<string, HTMLElement | null>;
631
+ readonly computed: <T = unknown>(fn: () => T) => T;
632
+ readonly effect: (fn: () => void | (() => void)) => void;
633
+ } & Omit<Signals, "app" | "context" | "request" | "computed" | "effect">;
552
634
  ```
553
635
 
554
- ### Using State
636
+ ### Using Signals
637
+
638
+ See the [Using Signals](#using-signals) section for more details on how to use signals in your components.
639
+
640
+ ### Using Refs
555
641
 
556
- Check the [Using State](#using-state) section for more details on how to use state in your components.
642
+ You can use `this.refs` to access refs in your components. Define refs in your component using the `ref` attribute:
643
+
644
+ ```tsx
645
+ function App(this: FC) {
646
+ this.effect(() => {
647
+ this.refs.input?.addEventListener("input", (evt) => {
648
+ console.log("Input changed:", evt.target.value);
649
+ });
650
+ });
651
+
652
+ return (
653
+ <div>
654
+ <input ref={this.refs.input} type="text" />
655
+ <button onClick={() => this.refs.input?.focus()}>Focus</button>
656
+ </div>
657
+ )
658
+ }
659
+ ```
557
660
 
558
661
  ### Using Context
559
662
 
@@ -567,7 +670,7 @@ function Dash(this: FC<{}, {}, { auth: { uuid: string; name: string } }>) {
567
670
  <h1>Welcome back, {auth.name}!</h1>
568
671
  <p>Your UUID is {auth.uuid}</p>
569
672
  </div>
570
- );
673
+ )
571
674
  }
572
675
 
573
676
  export default {
@@ -578,9 +681,9 @@ export default {
578
681
  {!auth && <p>Please Login</p>}
579
682
  {auth && <Dash />}
580
683
  </html>
581
- );
582
- },
583
- };
684
+ )
685
+ }
686
+ }
584
687
  ```
585
688
 
586
689
  ### Accessing Request Info
@@ -597,7 +700,7 @@ function RequestInfo(this: FC) {
597
700
  <p>{request.url}</p>
598
701
  <p>{request.headers.get("user-agent")}</p>
599
702
  </div>
600
- );
703
+ )
601
704
  }
602
705
 
603
706
  export default {
@@ -605,8 +708,8 @@ export default {
605
708
  <html request={req}>
606
709
  <RequestInfo />
607
710
  </html>
608
- ),
609
- };
711
+ )
712
+ }
610
713
  ```
611
714
 
612
715
  ## Streaming Rendering
@@ -626,8 +729,8 @@ export default {
626
729
  <p>After 1 second</p>
627
730
  </Sleep>
628
731
  </html>
629
- ),
630
- };
732
+ )
733
+ }
631
734
  ```
632
735
 
633
736
  You can set the `rendering` attribute to `"eager"` to force synchronous rendering (the `placeholder` will be ignored):
@@ -640,8 +743,8 @@ export default {
640
743
  <p>After 1 second</p>
641
744
  </Sleep>
642
745
  </html>
643
- ),
644
- };
746
+ )
747
+ }
645
748
  ```
646
749
 
647
750
  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:
@@ -657,8 +760,8 @@ export default {
657
760
  <html>
658
761
  <Hello catch={err => <p>{err.message}</p>} />
659
762
  </html>
660
- ),
661
- };
763
+ )
764
+ }
662
765
  ```
663
766
 
664
767
  ## Customizing html Response
@@ -678,8 +781,8 @@ export default {
678
781
  >
679
782
  <h1>Page Not Found</h1>
680
783
  </html>
681
- ),
682
- };
784
+ )
785
+ }
683
786
  ```
684
787
 
685
788
  ### Using htmx
@@ -705,9 +808,9 @@ export default {
705
808
  Click Me
706
809
  </button>
707
810
  </html>
708
- );
709
- },
710
- };
811
+ )
812
+ }
813
+ }
711
814
  ```
712
815
 
713
816
  #### Adding htmx Extensions
@@ -722,8 +825,8 @@ export default {
722
825
  Click Me
723
826
  </button>
724
827
  </html>
725
- ),
726
- };
828
+ )
829
+ }
727
830
  ```
728
831
 
729
832
  #### Specifying htmx Version
@@ -738,8 +841,8 @@ export default {
738
841
  Click Me
739
842
  </button>
740
843
  </html>
741
- ),
742
- };
844
+ )
845
+ }
743
846
  ```
744
847
 
745
848
  #### Installing htmx Manually
@@ -760,8 +863,8 @@ export default {
760
863
  </button>
761
864
  </body>
762
865
  </html>
763
- ),
764
- };
866
+ )
867
+ }
765
868
  ```
766
869
 
767
870
  ## License