Skip to content

ochairo/beat

beat

JSX framework built on Pulse for predictable apps with local updates.
Run-once components, local updates, explicit routing, async resources, and SSR.

npm version npm downloads CI License

Documentation

Overview

Beat is built for apps that should stay understandable as they grow. The public model stays explicit:

  • Run-once components — a component behaves like setup code, not a rerender loop
  • Local updates — Pulse exact-path subscriptions update only the binding that depends on the changed value
  • Explicit router and resources — routing state and async state stay in normal userland APIs
  • Shared client/server model — the same component tree and router power rendering, hydration, and SSR

Scaffold

Start a new app with the scaffolder:

pnpm dlx @ochairo/beat-create my-app

Use the showcases template for a full-featured app with routing, crypto dashboard, kanban board, and spreadsheet:

pnpm dlx @ochairo/beat-create my-app --template showcases

That command scaffolds a Vite + TypeScript starter already configured for Beat's JSX runtime and Vite plugin.

Example

import { component } from "@ochairo/beat";
import { pulse } from "@ochairo/pulse";

const counter = pulse(0);

const onclick = (value) => {
  counter.set(counter.get() + value);
};

export const App = component(() => {
  return (
    <main>
      <header class="header">
        <h1 class="title">Counter app</h1>
      </header>
      <section class="counter">
        <button class="button" onClick={() => onclick(-1)}>
          -
        </button>
        <strong class="result">{counter}</strong>
        <button class="button" onClick={() => onclick(1)}>
          +
        </button>
      </section>
    </main>
  );
});

Server-Side Rendering

Beat's SSR uses the same component tree and router — two entry points, no new framework.

// entry-server.ts
import { Window } from "happy-dom";
import { createRouter } from "@ochairo/beat";
import { renderToString, waitForRouter } from "@ochairo/beat/server";

export async function render(url: string): Promise<string> {
  const win = new Window({ url });
  globalThis.document = win.document as unknown as Document;

  const router = createRouter({ routes, initialUrl: url });
  await waitForRouter(router, { signal: AbortSignal.timeout(5_000) });
  const html = renderToString(() => <App router={router} />);

  win.happyDOM.close();
  return `<div id="app">${html}</div>`;
}

// entry-client.ts
import { createRouter, hydrate } from "@ochairo/beat";

const router = createRouter({ routes, window });
hydrate(document.getElementById("app")!, <App router={router} />);
  • initialUrl — resolves the route on the server without reading window.location
  • waitForRouter — waits for route loaders to settle before rendering; accepts AbortSignal
  • renderToString — takes a factory () => JSX; suppresses onMount on the server
  • hydrate — single atomic swap from server HTML to live Beat tree, no blank frame

About

Pulse-native JSX for SPA applications.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors