mono-jsx

repository·main·Indexed 19 days ago

https://github.com/ije/mono-jsx

A lightweight, zero-dependency JSX runtime for server-side rendering that renders <html> elements directly to Web Response objects. Designed for Deno, Bun, and Cloudflare Workers, it features streaming rendering, signals-based reactivity, and built-in routing without a virtual DOM. It supports standard HTML properties, async components, and two-way data binding via signals.

Tokens
14.6K
Snippets
56
Records
59
Agent score
68%

What's inside mono-jsx

  1. Use `<slot>` for component composition

    main

    Instead of React's children prop, mono-jsx uses the standard HTML <slot> element. You can define named slots using the name prop.

    function Container() {
      return (
        <div class="container">
          <slot />
          <slot name="desc" />
        </div>
      )
    }
    
    function App() {
      return (
        <Container>
          <p slot="desc">This is a description.</p>
          <h1>Hello world!</h1>
        </Container>
      )
    }
  2. Use advanced styling with pseudo-classes and media queries

    main

    The style property supports pseudo-classes, pseudo-elements, media queries, and CSS nesting directly within the object definition.

    <a
      style={{
        display: "inline-flex",
        gap: "0.5em",
        color: "black",
        "::after": { content: "↩️" },
        ":hover": { textDecoration: "underline" },
        "@media (prefers-color-scheme: dark)": { color: "white" },
        "& .icon": { width: "1em", height: "1em" },
      }}
    >
      <img class="icon" src="link.png" />
      Link
    </a>;
  3. Perform lazy rendering with the `<component>` element

    main

    Since mono-jsx renders HTML on the server and sends no hydration JavaScript, you can use the <component> element to request dynamic component rendering on the client. There are three ways to specify the component:

    1. By Name: Use the name prop. The component must be registered in the components prop of the root <html> element.
    2. By Function Reference: Use the is prop. This does not require registration in the <html> element.
    3. By JSX Element: Use the as prop to pass a pre-constructed JSX element.

    All methods support a pending prop to display fallback content while the component is loading.

    // 1. By Name (requires registration in <html>)
    function Foo(props: { bar: string }) {
      return <h1>{props.bar}</h1>;
    }
    
    export default {
      fetch: (req) => (
        <html request={req} components={{ Foo }}>
          <component name="Foo" props={{ bar: "baz" }} pending={<p>Loading...</p>} />
        </html>
      )
    }
    
    // 2. By Function Reference (no registration needed)
    export default {
      fetch: (req) => (
        <html request={req}>
          <component is={Foo} props={{ bar: "baz" }} pending={<p>Loading...</p>} />
        </html>
      )
    }
    
    // 3. By JSX Element
    export default {
      fetch: (req) => (
        <html request={req}>
          <component as={<Foo bar="baz" />} pending={<p>Loading...</p>} />
        </html>
      )
    }
  4. Handle events and form actions

    main

    Event handlers (e.g., onClick) are serialized to strings and sent to the client.

    CRITICAL: Event handlers run in the browser. You cannot use server-side variables, imports, or server-only APIs (like Deno.exit) inside them. Only browser-native APIs (like document or evt) are available.

    For forms, you can pass a function to the action prop of a <form>. This function is called on submission and receives a FormData object.

    // Form Action
    function App() {
      return (
        <form action={(data: FormData) => console.log(data.get("name"))}>
          <input type="text" name="name" />
          <button type="submit">Submit</button>
        </form>
      );
    }
    
    // Event Handler
    function Button() {
      return (
        <button onClick={(evt) => alert("BOOM!")}>
          Click Me
        </button>
      );
    }
  5. Access scoped signals and context via `this` in components

    main

    In mono-jsx, component functions have a scoped signals object bound to this. This allows direct access to signals, context, request information, and session data without passing them as props.

    To use TypeScript with this pattern, use the provided helper types like FC, WithContext, WithRefs, or WithAppSignals to type the this context of your component functions.

    type FC<Signals = {}, Refs = {}> = {
      init(initValue: Signals): void;
      app: AppSignals & { refs: AppRefs; url: WithParams<URL> }
      context: Context;
      request: WithParams<Request>;
      session: Session;
      refs: Refs;
      computed<T = unknown>(fn: () => T): T;
      $: FC["computed"];
      effect(fn: () => void | (() => void)): void;
    } & Signals;
    
    // Example usage with typing
    function Component(this: WithContext<FC, { secret: string }>) {
      console.log(this.context.secret);
    }
  6. How signals work in mono-jsx

    main

    mono-jsx uses signals to manage state and update the view efficiently. Signals are bound to the component instance via the this keyword. When a signal is updated, the view automatically re-renders. Signals can be managed at the component level, the application level, or as derived (computed) values.

    function Counter(this: FC<{ count: number }>, props: { initialCount?: number }) {
      this.count = props.initialCount ?? 0;
      return (
        <div>
          <span>{this.count}</span>
          <button onClick={() => this.count++}>+</button>
        </div>
      )
    }
  7. Manage form submitting state

    main

    When a <form route> is submitted, mono-jsx automatically manages the client-side submitting state:

    • Adds a CSS class submitting to the form.
    • Disables all form controls to prevent double submission.
    • Clears local <formslot> elements before inserting the new response.

    You can customize the class name using the data-submitting-class attribute.

    <form route data-submitting-class="is-loading">
      <input type="email" name="email" required />
      <button type="submit">Subscribe</button>
      <formslot />
    </form>
  8. Use Async Components and Streaming

    main

    Async components can return a Promise or an async generator. mono-jsx supports streaming rendering, allowing you to fetch data or stream content (like LLM tokens) to the client.

    Loading States

    Use the pending prop to show a fallback UI while an async component is resolving.

    Synchronous Rendering

    Use the rendering="eager" prop to force a component to render synchronously, which ignores the pending property.

    // Async Component with Promise
    async function JsonViewer({ url }: { url: string }) {
      const data = await fetch(url).then((res) => res.json());
      return <ObjectViewer data={data} />;
    }
    
    // Async Generator for Streaming (e.g. LLM)
    async function* Chat({ prompt }: { prompt: string }) {
      const stream = await openai.chat.completions.create({ ... stream: true });
      for await (const event of stream) {
        if (event.choices[0]?.delta.content) {
          yield <span>{event.choices[0].delta.content}</span>;
        }
      }
    }
    
    // Usage with pending state
    <Chat prompt="Tell me a story" pending={<span>●</span>} />
    
    // Usage with eager rendering
    <Sleep ms={1000} rendering="eager" />
  9. Control form response placement with <formslot>

    main

    By default, HTML returned from a FormHandler is appended to the form. Use the <formslot> element to control exactly where response content is inserted.

    Modes

    • replaceChildren (default): Replaces the children of the <formslot>.
    • insertafter: Inserts HTML after the <formslot>.
    • insertbefore: Inserts HTML before the <formslot>.

    Named Slots

    You can use the name prop on <formslot> in the JSX and the formslot prop on returned elements in the FormHandler to target specific locations.

    Special Values

    The formslot prop on returned elements also accepts these special values to replace larger parts of the UI:

    • :form: Replaces the entire <form> element.
    • :router: Replaces the children of the current <router>.
    • :root: Replaces the children of the page (the root).
    function MyRoute(this: FC) {
      return (
        <form route>
          <formslot name="info" />
          <input type="text" name="message" />
          <button type="submit">Send</button>
        </form>
      )
    }
    
    MyRoute.FormHandler = function(this: FC, data: FormData) {
      return <p formslot="info">This is info message</p>
    }
  10. Use standard HTML properties and class composition

    main

    Unlike React, mono-jsx uses standard HTML property names:

    • Use class instead of className
    • Use for instead of htmlFor
    • Use onInput instead of onChange

    You can compose the class property using arrays of strings, objects, or expressions:

    <div
      class=[
        "container box",
        isActive && "active",
        { hover: isHover },
      ]}
    />;
    <div
      class=[
        "container box",
        isActive && "active",
        { hover: isHover },
      ]
    />;
  11. Implement View Transitions

    main

    To enable smooth transitions between views, add the viewTransition prop to specific components like <show>, <hidden>, <switch>, <component>, or <router>. You can pass a transition name string or set it to true while defining the transition in the style property.

    Supported components:

    • <show viewTransition="name">
    • <hidden viewTransition="name">
    • <switch viewTransition="name">
    • <component viewTransition="name">
    • <router viewTransition="name">
    function App(this: FC<{ message: string }>) {
      this.message = "Hello world!";
      return <h1 viewTransition="fade">{this.message}</h1>;
    }
    
    // Or using style for custom names
    <h1 viewTransition style={{ viewTransitionName: "fade" }}>{this.message}</h1>
  12. Handle 404 Fallbacks in the Router

    main

    To display content when no route matches the current URL, pass children to the <router> element. This content acts as a fallback (404) view.

    <router>
      <p>Page Not Found</p>
      <a href="/">Back to Home</a>
    </router>