Ajo

Installation

Add Ajo to a project, configure the JSX runtime and render a component.

A paper kite shaped like an application window, flying over a dune by the sea.A paper kite shaped like an application window, flying over a dune by the sea.

Ajo is a small library for building user interfaces with JSX and JavaScript generators. A component is either a plain function or a generator function, and Ajo updates the page in place, reusing the DOM nodes that are already there. This page sets up a project, configures JSX and renders a first pair of components.

Install the package

Ajo is a single package with no runtime dependencies.

Terminal
pnpm add ajo@0.2.0

You import from the first three entry points; your build tool imports the JSX runtime.

ImportContains
ajorender and the TypeScript types.
ajo/contextcontext and current, for values shared down a tree.
ajo/htmlrender, which turns components into HTML on a server.
ajo/jsx-runtimeThe automatic JSX runtime. ajo/jsx-dev-runtime exports the same functions.

Configure JSX

Ajo uses the automatic JSX runtime: the compiler turns every JSX expression into a call to a function it imports from ajo/jsx-runtime on its own. Components never import anything to use JSX. Tell your tools that the JSX import source is ajo, in tsconfig.json.

tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "ajo"
  }
}

TypeScript uses it to check JSX and to resolve element types in your editor, and Vite reads it for .ts and .tsx files. The value react-jsx is only TypeScript’s name for the automatic runtime mode; the import source decides which library provides it, and nothing here involves React.

For .jsx files, or Vite projects without a tsconfig, set oxc: { jsx: { importSource: 'ajo' } } in vite.config.ts. Other build tools take the same two settings: jsx set to react-jsx or automatic (the name depends on the tool) and jsxImportSource set to ajo.

Mount an application

Give the page an element to render into and load a module script:

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Journal</title>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>
</html>
src/main.tsx
import { render } from 'ajo'
import { Journal } from './journal'

render(<Journal />, document.getElementById('root')!)

The call render(children, container) reconciles the container’s child nodes with the JSX you pass: it reuses nodes that match, creates the missing ones and removes the rest. Mounting and updating are the same operation, so calling render again on the same container updates it in place, and a call with null empties the container and ends the components that were inside.

Two optional arguments limit the work to a range of the container’s children. With render(children, container, child, ref), Ajo starts at the node child and stops before the node ref, leaving everything outside the range untouched. This helps when a container also holds content Ajo does not manage, or when only part of some server-rendered HTML should be taken over.

src/main.tsx
import { render } from 'ajo'
import { Journal } from './journal'

const header = document.querySelector('body > header')!
const footer = document.querySelector('body > footer')!

// Reconcile only what lies between the static header and footer.
render(<Journal />, document.body, header.nextSibling, footer)

A first pair of components

A stateless component is a function from its args to JSX. A stateful component is a generator function: variables declared before its loop hold the component’s state, and every pass through the loop yields what it shows. Calling this.next() applies a change and renders the component again.

src/journal.tsx
import type { Stateful, Stateless } from 'ajo'

type EntryArgs = { title: string; date: string }

const Entry: Stateless<EntryArgs> = ({ title, date }) => (
  <article class="entry">
    <h2>{title}</h2>
    <time datetime={date}>{date}</time>
  </article>
)

export const Journal: Stateful = function* () {

  const entries = [{ id: 1, title: 'First morning pages', date: '2026-09-21' }]
  let title = ''

  const add = () => this.next(() => {
    if (!title.trim()) return
    entries.unshift({ id: Date.now(), title, date: new Date().toISOString().slice(0, 10) })
    title = ''
  })

  while (true) yield (
    <>
      <input
        placeholder="Title of a new entry"
        set:value={title}
        set:oninput={e => title = (e.target as HTMLInputElement).value}
      />
      <button set:onclick={add}>Add entry</button>
      <ul>
        {entries.map(entry => (
          <li key={entry.id}>
            <Entry title={entry.title} date={entry.date} />
          </li>
        ))}
      </ul>
    </>
  )
}

Ajo wraps each stateful component in a host element, a <div> unless you choose another tag, and this inside the generator is that element. Typing into the input only updates the local title variable. The button calls this.next(), which runs the update and renders the list again. The key on each list item lets Ajo keep every entry’s DOM node when a new entry is added at the top.

With Ajo Kit

An Ajo Kit application comes set up for Ajo. Its project setup includes the JSX configuration, and Kit calls the render functions for you, on the server and in the browser, for every route. If you are building an application rather than adding Ajo to an existing page, start with Project setup.

Full reference: ajo LLMs.md