Skip to content
MarcisbeePublic

About

๐ŸŒ€Tiny (in size) front-end framework with no extra browser re-flows

Topics

Resources

Stars

945 stars

Watchers

38 watching

Forks

Latest commit

ย 

History

568 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Radi

Radi is a lightweight reactive JSX runtime that lets you write components as plain functions with direct DOM manipulation โ€” no virtual DOM required.

Start a project

With Node.js 22+ installed, choose a starter:

npm create radi@latest -- my-app --template spa
npm create radi@latest -- my-server --template ssr
npm create radi@latest -- my-site --template ssg
npm create radi@latest -- my-go-app --template go

Then enter the generated directory, run npm install, and run npm run dev. Open http://127.0.0.1:3000. The generator refuses nonempty directories and never installs dependencies automatically. Use npm create radi@latest -- --help for syntax.

Template Result Production build
spa Browser-rendered app with a reactive counter Static dist/
ssr Node HTTP server rendering HTML per request dist/ plus npm dependencies
ssg Two pages generated at build time Static dist/
go Go HTTP server rendering TSX in embedded V8 Standalone server binary

The Go starter additionally requires Linux amd64 with glibc, Go 1.26.10, Clang 21, and CGO. Follow its generated README before building. All starters include npm run build and npm start; their READMEs explain editing, previewing, and deployment. The generator and templates ship inside the create-radi npm package, published alongside the matching Radi release.

Radi also supports synchronous server rendering and ordered async HTML streaming. See server rendering for APIs, runtime examples, limitations, and reproducible performance measurements.

For a Go HTTP server with embedded V8, async Flop access and TSX components, see the Go adapter.

For lazy interactions on existing server DOM, see resumption. Explicit boundaries carry JSON state and load named handlers on the first click.

For documentation and other static sites with interactive islands, see static site generation.

For SPA navigation, lazy route modules, loaders, and actions, see client routing and route data.

What Makes Radi Different

Radi takes a unique approach compared to React, Preact, or even Solid:

Feature React/Preact Solid Radi
Virtual DOM Yes No No
Props Object Object Function () => props
Component execution Every render Once Once
Reactivity Implicit (re-render all) Signals Explicit functions
Update mechanism Scheduler Signals Native DOM events

Props Are Functions

In Radi, components receive props as a getter function, not a plain object:

function Greeting(props: JSX.Props<{ name: string }>) {
  // Access props by calling the function
  return <h1>Hello, {() => props().name}!</h1>;
}

This enables fine-grained reactivity โ€” when parent state changes, only the specific reactive expressions that read props will update.

Components Are Stateful (Run Once)

Component functions execute exactly once. The returned JSX is the component's template. State lives in closure variables:

function Counter(this: ComponentNode) {
  // This code runs once when the component mounts
  let count = 0;

  return (
    <button onclick={() => { count++; update(this); }}>
      {() => `Count: ${count}`}
    </button>
  );
}

Reactive Places Are Functions

Wrap any expression in a function to make it reactive. When update() is called on an ancestor, these functions re-execute:

const view = (
  <div>
    {/* Static โ€” never updates */}
    <span>Static text</span>

    {/* Reactive โ€” re-runs on update */}
    <span>{() => dynamicValue}</span>

    {/* Reactive prop */}
    <input value={() => inputValue} />
  </div>
);

Updates Use Native Events

Radi uses native DOM events (update, connect, disconnect) instead of a custom scheduler. Call update(node) to trigger reactive re-evaluation:

function Timer() {
  let seconds = 0;
  const el = <div>{() => seconds}</div>;

  setInterval(() => {
    seconds++;
    update(el); // Dispatches native "update" event
  }, 1000);

  return el;
}

Features

  • Direct DOM manipulation with minimal abstraction
  • Fine-grained reactivity via functions
  • Lightweight component model with closures for state
  • Fragment support (<Fragment> or <>...</>)
  • Lifecycle events (connect, disconnect)
  • AbortSignal helpers tied to element lifecycle
  • Suspense boundaries for async components
  • Keyed lists for efficient reconciliation
  • TypeScript automatic JSX runtime support

Installation

# Deno
deno add jsr:@Marcisbee/radi

# npm
npm install radi

Basic Usage

import { createRoot, update } from 'radi';

function App() {
  let count = 0;

  const root = (
    <div>
      <h1>Counter App</h1>
      <p>Count: {() => count}</p>
      <button onclick={() => { count++; update(root); }}>
        Increment
      </button>
    </div>
  );

  return root;
}

const root = createRoot(document.getElementById('app')!);
root.render(<App />);

Component Anatomy

function MyComponent(
  this: ComponentNode,           // Host element (the <host> tag)
  props: JSX.Props<{ value: number }> // Props getter function
) {
  // 1. Setup code runs once
  const signal = createAbortSignal(this);

  // 2. Event handlers
  this.addEventListener('connect', () => {
    console.log('Component mounted');
  }, { signal });

  // 3. Return JSX template
  return (
    <div>
      {/* Reactive expression */}
      {() => props().value * 2}
    </div>
  );
}

Fragments

const list = (
  <>
    <li>First</li>
    <li>Second</li>
  </>
);

Reactive Children

Any function passed as a child is treated as a reactive generator:

const time = () => new Date().toLocaleTimeString();

const clock = <div>The time is: {time}</div> as HTMLElement;
setInterval(() => update(clock), 1000);

Reactive Props

Props can also be reactive functions:

let isDisabled = false;

const button = (
  <button disabled={() => isDisabled}>
    Click me
  </button>
) as HTMLElement;

// Later...
isDisabled = true;
update(button);

Lifecycle Events

Elements receive connect / disconnect events when added/removed from the document:

const node = (
  <div
    onconnect={() => console.log('connected')}
    ondisconnect={() => console.log('disconnected')}
  />
);

AbortSignal Helpers

Automatically clean up event listeners and subscriptions:

function Component(this: HTMLElement) {
  const signal = createAbortSignal(this);

  // Automatically removed when component disconnects
  window.addEventListener('resize', handleResize, { signal });

  return <div>...</div>;
}

For cleanup on update or disconnect:

const signal = createAbortSignalOnUpdate(element);
// Aborts when element updates OR disconnects

Memoization

Skip re-computation when values haven't changed:

const expensiveChild = memo(
  () => <ExpensiveComponent data={data} />,
  () => data === previousData // Return true to skip update
);

Keyed Lists

For efficient list reconciliation:

import { createList, createKey } from 'radi';

function TodoList(props: () => { items: Todo[] }) {
  return (
    <ul>
      {() => createList((key) =>
        props().items.map((item) =>
          key(() => <TodoItem item={item} />, item.id)
        )
      )}
    </ul>
  );
}

Single keyed elements preserve state across updates:

{() => createKey(() => <Editor />, activeTabId)}
// Editor remounts only when activeTabId changes

Async Components & Suspense

Radi supports async components with Suspense boundaries:

import { Suspense, suspend, unsuspend } from 'radi';

// Async component using Promises
async function UserProfile(props: JSX.Props<{ userId: number }>) {
  const response = await fetch(`/api/users/${props().userId}`);
  const user = await response.json();

  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

// Wrap in Suspense for loading state
const app = (
  <Suspense fallback={() => <div>Loading...</div>}>
    <UserProfile userId={123} />
  </Suspense>
);

Manual suspend/unsuspend for custom async work:

function DataLoader(this: HTMLElement) {
  let data = null;

  suspend(this);
  fetchData().then((result) => {
    data = result;
    unsuspend(this);
    update(this);
  });

  return <div>{() => data ? renderData(data) : null}</div>;
}

TypeScript JSX Configuration

Automatic Runtime (Recommended)

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "radi"
  }
}

For development with extra source metadata:

{
  "compilerOptions": {
    "jsx": "react-jsxdev",
    "jsxImportSource": "radi"
  }
}

Manual Mode

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "createElement",
    "jsxFragmentFactory": "Fragment"
  }
}

Then import manually:

import { createElement, Fragment } from 'radi';

API Reference

Rendering

  • createRoot(target) โ€” Create a render root
  • root.render(element) โ€” Render into the root
  • root.unmount() โ€” Unmount and cleanup

Reactivity

  • update(node) โ€” Trigger reactive updates on node and descendants
  • memo(fn, shouldMemo) โ€” Memoize reactive expressions

Lists

  • createList(fn) โ€” Create keyed list for efficient diffing
  • createKey(renderFn, key) โ€” Create single keyed element

Lifecycle

  • createAbortSignal(node) โ€” AbortSignal that fires on disconnect
  • createAbortSignalOnUpdate(node) โ€” AbortSignal that fires on update or disconnect

Suspense

  • Suspense โ€” Boundary component with fallback
  • suspend(node) โ€” Signal async work starting
  • unsuspend(node) โ€” Signal async work complete

JSX

  • createElement(type, props, ...children) โ€” JSX factory
  • Fragment โ€” Fragment symbol

Contributing

For reproducible browser performance comparisons with React, Preact, Vue, and vanilla JavaScript, see the comparison benchmark suite.

  1. Fork and clone
  2. Install dependencies
  3. Run tests: deno task test
  4. Open a PR with a concise description

For publishing to npm, see the release workflow.

License

MIT

About

๐ŸŒ€Tiny (in size) front-end framework with no extra browser re-flows

Topics

Resources

Stars

945 stars

Watchers

38 watching

Forks

Releases

Packages

Used by

Contributors

Languages