Library / SDK
octanejs/octane avatar
octanejs/octane

Octane: hooks keyed to call sites, a .tsrx dialect, and a monorepo whose test line is four checks long

Project brief: React s programming model, compiled. The successor to Inferno, carrying its performance-first goal forward:.

1,457 stars62 forksTypeScriptMIT

At a glance

What is it?
A React-shaped UI framework that compiles components into direct DOM code, written by Dominic Gannaway as the successor to Inferno. Hooks are tracked by call site rather than call order, dependency arrays can be omitted, .tsrx adds template directives to JSX, and the manifest requires Node.js 22.22.2 before vitest ever runs.
Who is it for?
Octane earns attention in codebases that want React's shape without React's runtime, and it is a poor match for anything built on class components, Server Components or synthetic events, because those are gone rather than merely undocumented.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Hooks key on call site, so one can sit inside an if

Octane tracks hooks by the place they are called from rather than by the order the calls arrive in. The stated consequence is that a hook can live inside an if or after an early return, and it keeps its state across renders that skip it. That is the change with the widest reach for code ported from React, because it removes the ordering discipline that React's rules of hooks impose and that most existing code follows out of habit. Exactly one rule survives. A hook inside a plain JavaScript loop is a compile error, on the grounds that every iteration would share one call-site slot. The way past it is the keyed @for directive instead of a map with a hook in the body, where each item gets its own hook state. So the framework does not reorder or re-key hooks, it declines to enforce an ordering it never relied on. Porting means auditing loops rather than auditing component top level.

Dependency arrays are derived, and written ones still mean React

Omit the array from useEffect, useMemo, useCallback and their neighbours and the compiler derives it from what the closure actually captures, including stable setters, dispatchers, refs and state getters. That is where the no-bookkeeping experience comes from, without leaving the hooks model. Arrays you write out keep exactly the meaning they have in React, which means a codebase can migrate one component at a time instead of all at once, and a team can keep the explicit form wherever a hand-written array is load-bearing. The same care about stale reads shows up in the return value: useState and useReducer hand back three items, [state, update, getState], and the third one is the practical part, since a delayed callback can read the current value instead of a capture that went stale. Nearby, useLinkedState resets or adjusts local state as soon as an input changes, with no effect and no state update during render.

.tsrx keeps standard JSX and adds template directives

Plain JSX runs: a component pasted from the React docs into a .tsx file works. The second dialect, .tsrx, is called the spiritual successor to JSX, and it adds template directives that compile to keyed fast paths. Four are named, @if, @for, @switch and @try, plus an @{ … } shorthand that puts setup code next to the output it feeds. The counter in the file is written in that dialect rather than in React JSX:

jsx
import { useState } from 'octane';

export function Counter() @{
	const [count, setCount] = useState(0);

	<button onClick={() => setCount(count + 1)}>
		{'Count: ' + count}
	</button>
}

The two dialects mix inside one application and imports cross the boundary, so a .tsx component can use a .tsrx one. Editor support is a separate VS Code extension named TSRX.tsrx-vscode-plugin, which adds highlighting, diagnostics, navigation and completions for those files. Neither the extension nor the new dialect is required by the runtime, which matters for teams that will not install editor tooling.

The engine floor names a patch release, not a major version

The install section opens with a requirement stated to the patch: Octane's published packages need Node.js 22.22.2 or newer, and the monorepo manifest agrees, with engines node set to >=22.22.2 alongside a packageManager pin of [email protected]. A major-only floor would have read as 22 or newer; naming a patch refuses a machine on 22.21, which is the kind of precision that CI enforces on developers and that container images quietly violate. The same section offers a scaffold that already runs, npm create octane my-app, followed by cd my-app and npm run dev. Two templates exist: --template spa gives a client-only application, --template fullstack adds routing, streaming server rendering, hydration and a production build, and leaving the flag off makes the generator ask. That is the only place in the file where a build mode is chosen by a question rather than a default. The claim that the React API transfers is not left as a slogan either: coverage is tracked case by case against a large behavioral suite in a generated parity report at docs/react-parity-coverage.md, rather than inferred from how many tests exist.

Three install routes, three package managers

In a project you already have, the file hands the job to a CLI, pnpm dlx @octanejs/cli init, which wires the project up including the TypeScript settings that .tsrx needs. Doing it by hand is one install command and two files:

bash
pnpm add octane @octanejs/vite-plugin
ts
// vite.config.ts
import { defineConfig } from 'vite';
import { octane } from '@octanejs/vite-plugin';

export default defineConfig({
	plugins: [octane()],
});
ts
// main.ts
import { createRoot } from 'octane';
import { App } from './App.tsrx';

const root = createRoot(document.getElementById('root')!);
root.render(App, { title: 'Hello world!' });

Vite is the plugin in the hand-written route, and Rspack and Rsbuild are supported too, with the getting started document covering all three build tools plus server rendering, hydration, streaming, deferred hydration and profiling. Watch the package manager drift: the scaffold says npm, the CLI route says pnpm dlx, the manual route says pnpm add, and the repository pins [email protected]. Every route ends with the same two obligations, an entry point that calls createRoot against a root element, and a build tool that has to run the Octane plugin.

pnpm test runs four checks before vitest

The root test script is not a test command. Read left to right it is pnpm three:upstream:check, then pnpm --dir packages/visx compile:check, then pnpm --dir packages/visx types:check, then pnpm --dir packages/streamdown types:check, and only then vitest run, with && between them so any failure stops the line before a single test executes. The upstream check is itself a package script, pnpm --dir packages/three upstream:check, so a package carried inside the monorepo is compared against upstream before the suite starts. Beside it sit a bindings audit and a staged DOM check, each with its own node --test file. Three vitest configs sit at the root, vitest.config.js, vitest.ci-sharded.config.js and vitest.react-parity.config.js, while the test line names a bare vitest run and no config. The visible end of the scripts object cuts off mid-entry, so whether the parity config is wired to a script cannot be read from the manifest here. Measurement sits beside all of it: bench runs pnpm --filter @benchmarks/news bench, bench:all runs node benchmarks/bench.mjs, bench:spa-navigation:typecheck type-checks one benchmark directory, and bench:svelte-check filters every svelte package under benchmarks. Five lynx:demo scripts filter a package called @octanejs/rspeedy-plugin, which the file never mentions.

The renderer is narrower than React, and the parity table has no rows

The omissions are on purpose: no class components, no Server Components, no synthetic event system, described in the file as choices rather than gaps and written down in a differences document on the project site. What replaces the synthetic layer is the platform itself, with real delegated DOM events and controlled form components on native events, carrying React's value and checked semantics with onInput firing per edit and native onChange on commit. Refs are plain props, so ref={cb}, ref={obj} and even ref={[a, b]} are all valid. Rendering has no virtual DOM at all: components re-render the React way while a compiled render path and an LIS-based keyed reconciler keep overhead down. Deferred hydration is a separate mode, where <Hydrate> keeps server HTML visible but inert and splits its children into their own chunk by default, and behavior-only roots attach abortable behavior and delegated events to markup they do not render or own. The React interoperability section then stops after its table header: the visible line is | Boundary | What it renders | React version, with no rows under it, so ReactCompat and OctaneCompat, both imported from octane/react, are named without a single shown boundary.

More than twenty examples, and five agent instruction files

The examples directory holds cartlane, catalog, cinebase, draftboard, electron, flowboard, gridlab, hacker-news, harbor, lexical-playground, mailroom, monaco-playground, pagecraft, pulseboard, relay, signal-chat, streambox, threadline, wayfinder and workbench, next to a shared directory, a catalog.json, an example.schema.json and an examples README. The names say what the framework is meant to sit inside: an editor, a spreadsheet grid, a chat client, a mail client, a desktop shell. The test line also reaches packages/visx and packages/streamdown, so third-party packages sit beside the runtime rather than being consumed from a registry. Alongside examples and packages the root carries benchmarks, playground, experiments, patches, scripts, test-utils, website and website-mcp, so documentation, benchmarks and the site are built from the same tree as the framework. At the root the same repository keeps AGENTS.md, CLAUDE.md, GEMINI.md, CONTEXT.md and CONTRIBUTING.md alongside directories named .agents, .claude, .codex, .cursor, .gemini and .rulesync, plus a rulesync.jsonc. One project, several agent instruction files, and no line in the file explaining how they are kept in step. The stated aim is developer experience, and the example count is where that aim is easiest to check.

Editorial conclusion

Octane earns attention in codebases that want React's shape without React's runtime, and it is a poor match for anything built on class components, Server Components or synthetic events, because those are gone rather than merely undocumented. Check the two places the file stops short before committing: the React interoperability table has headers and no rows, so both compat wrappers are asserted rather than shown, and the evidence for the React API claim sits in a generated parity report under docs. Pin Node.js 22.22.2 or newer or the install is refused, and budget for a test command that runs four type and compile checks before vitest starts.

Frequently asked questions

What is Octane and who created it?

Octane is a fast JavaScript UI framework and the successor to Inferno, created by Dominic Gannaway, who also created Inferno and has worked on React, Lexical, Ripple and Svelte. You write components with the React API you already know, and a compiler turns them into direct DOM code before they ship, with no virtual DOM and no dependency arrays to maintain by hand.

How do I add Octane to a project I already have?

Run pnpm dlx @octanejs/cli init, which wires the project up including the TypeScript settings that .tsrx needs. By hand it is pnpm add octane @octanejs/vite-plugin, the octane() plugin in vite.config.ts, and an entry point that calls createRoot against a root element. Rspack and Rsbuild are supported as well.

Which Node.js version does Octane require?

The published packages need Node.js 22.22.2 or newer, stated to the patch release, and the monorepo manifest sets engines node to >=22.22.2 with [email protected] as the package manager.

Does Octane support class components or synthetic events?

No, and the file calls that a choice rather than a gap. There are no class components, no Server Components and no synthetic event system; events are real delegated DOM events, controlled forms follow React's value and checked semantics with onInput per edit and native onChange on commit, and refs are plain props including ref={cb}, ref={obj} and ref={[a, b]}.

What is the .tsrx file extension in Octane?

It is a second authoring dialect described as the spiritual successor to JSX, with template directives @if, @for, @switch and @try that compile to keyed fast paths, plus an @{ … } shorthand that puts setup next to the output. Standard .tsx keeps working, and the two dialects can be mixed in one application with imports crossing the boundary.

Official sources

  1. Official documentation
  2. Official README
  3. Project repository
  4. Release notes
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/octanejs-octane.svg)](https://hysenlabs.com/projects/octanejs-octane)