Getting Started With Octane, Inferno's Successor
Octane, Infernos successor, compiles React-style components to direct DOM code and explains hooks, dependency arrays, setup, and beta status.
Octane is a JavaScript UI framework from Dominic Gannaway that takes components written with React’s API (useState, useEffect, memo, context, portals, Suspense) and compiles them ahead of time into direct DOM code, so there is no virtual DOM in what ships to the browser.
If you have worked with React for a while, some of its rules can start to feel like part of the model itself: hooks run in the same order every render, dependency arrays are hand-maintained, and a conditional effect becomes an extracted child component or a guard clause inside the effect body. Most of them are there to keep a runtime reconciler happy, and the reconciler is the piece Octane removes.
This piece covers what Octane changes, which of those changes you notice first, how to get it running, and how far along the project is.
Key Takeaways
- Octane compiles React-style components into direct DOM operations, removing the virtual DOM from the shipped runtime.
- A hook’s identity comes from where it sits in the source, not from the order hooks run in, so a hook can sit inside an
ifbranch or after an early return; plain JavaScript loops are the one placement the compiler rejects. - You can leave a dependency array out and let the compiler read the closure for you. Write one yourself and it means what it means in React; pass
nullto run on every render. - You need Node.js 22.22.2 or newer for the published packages, and
npm create octane my-appscaffolds a project. - Octane calls itself beta software: the runtime, compiler and SSR/hydration paths work, but APIs are still moving.
What Is Octane, and Who Built It?
Octane positions itself as the successor to Inferno: the React API you already know, with the compiler taking over three jobs the React runtime does today, namely the virtual DOM, hook ordering and dependency arrays. Gannaway built Inferno, and his other work includes React, Lexical, Ripple and Svelte.
That lineage is the reason this is worth ten minutes rather than a bookmark. Inferno pursued speed by making a virtual DOM implementation as fast as one could reasonably be, which is the thesis our earlier look at Inferno.js examined. Octane keeps the performance-first goal and inverts the mechanism: instead of a faster diff, no diff. The successor framing comes from Octane’s own materials; there is no corresponding announcement on the Inferno side.
The Compiler Idea: No Virtual DOM at Runtime
Where React builds a tree of element descriptions on each render and reconciles it against the previous tree, Octane compiles each template into a DOM node that gets cloned at runtime and patched directly. The project’s documentation site is itself built with Octane, which is a reasonable signal that the compiler handles a non-trivial application.
The practical consequence is that the work React does at runtime (walking a tree, comparing props, deciding what changed) is decided at build time instead. The project publishes a benchmark grid on its homepage, normalised against Octane across several suites. Those are the project’s own figures, measured by the project, and the page does not publish hardware or a run date, so treat them as a claim to verify rather than an independent result.
Hooks Tracked by Call Site, Not Call Order
Octane gives each hook its identity from the place it appears in the source rather than from the order hooks run in, and it works out omitted effect and memo dependency lists from the closure. That is why a hook behind a condition is fine. This is the change with the most day-to-day consequence.
Here is the shape you write in React, where the hook has to run unconditionally:
function Panel({ isEditing }: Props) {
const [draft, setDraft] = useState('');
useEffect(() => {
if (!isEditing) return;
syncDraft(draft);
}, [isEditing, draft]);
if (!isEditing) return <Readonly />;
return <Editor value={draft} onChange={e => setDraft(e.target.value)} />;
}
The state and the effect are hoisted above the branch that needs them, and the branch logic is repeated inside the effect. In Octane, the hook goes where it belongs:
function Panel({ isEditing }: Props) {
if (!isEditing) return <Readonly />;
const [draft, setDraft] = useState('');
useEffect(() => syncDraft(draft));
return <Editor value={draft} onInput={e => setDraft(e.currentTarget.value)} />;
}
What this removes in practice: the child component extracted purely to make a hook conditional, the “hook always runs and conditionally does nothing” pattern, and the ternaries that exist to keep a call count stable. Note that Octane’s events come straight from the DOM, so reach for onInput when you want an update on every keystroke, while onChange fires when the browser commits the edit.
The single restriction the project names is plain JavaScript loops. Hooks are keyed by compiler-assigned call site, so a slot-keyed hook inside a for loop has no stable identity and the compiler rejects it. A keyed list in the template or a child component per item is the way through.
Why Are Dependency Arrays Optional in Octane?
Leave the list off and the compiler works it out from the closure. Write the array yourself and it behaves exactly as it does in React; pass null when you want the work to run on every render. This covers useEffect, useMemo, useCallback and the other hooks that take a list.
// React: you maintain the list
useEffect(() => {
socket.subscribe(roomId, onMessage);
}, [socket, roomId, onMessage]);
// Octane: the compiler reads what the closure captured
useEffect(() => {
socket.subscribe(roomId, onMessage);
});
The escape hatch matters as much as the inference: an explicit array is never rewritten, so anywhere you want exact control, write it. Direct calls to the built-in hooks keep this inference in any module the compiler processes, custom hooks in plain .ts or .js included. Calls to a wrapper of your own are a narrower case: the wrapper has to be declared locally in a fully compiled .tsrx or .tsx module, and it has to pass its callback and its last dependency parameter straight through to a supported hook.
How Do You Get octanejs Running?
You need Node.js 22.22.2 or newer for the published packages. The octane create command takes --template spa for a client-only app or --template fullstack for routing, streaming SSR, hydration and a production build; skip the flag and it asks you.
npm create octane my-app
cd my-app
npm run dev
Whichever package manager you run the command with is the one that installs the dependencies, because a brand new directory has no lockfile to read, which is why the docs and the repo show different package managers for the same step. For a project you already have, the quick start guide covers the Vite path: install octane and @octanejs/vite-plugin, then add the plugin. That plugin brings the compiler with it. Rspack uses @octanejs/rspack-plugin and Rsbuild uses @octanejs/rsbuild-plugin.
TSRX, Briefly
TSRX is the syntax Octane components are authored in, carried in .tsrx files, adding template directives (@if, @for, @switch, @try) and scoped <style> blocks next to the markup they apply to. It is a language project in its own right rather than an Octane feature, and Octane is one of its compile targets alongside React, Preact, Solid, Vue and Ripple. It also adds @{ ... }, a shorthand for a function body that returns a single JSX element or fragment, with setup at the top and the final node as the output. You are not obliged to adopt it: the TSRX vs TSX/JSX guide makes the point that the two dialects share the same hooks, context, portals, Suspense, transitions, native events, scoped styles, server rendering and hydration, and its own advice is to leave working TSX alone rather than change extensions for the sake of it.
Where Does Octane Actually Stand?
The project calls Octane beta software: the runtime, compiler and SSR/hydration paths all work, but the APIs can still move before 1.0. Octane’s changelog puts the current releases in the 0.3 line, and the quick start’s advice to pin versions in anything real follows from that. By the project’s own count the core suite runs more than 3,900 separate behavioural tests, spread across conformance, differential, hydration, runtime, compiler and SSR checks. How much of React’s own coverage that represents is tracked case by case in a generated parity report rather than read off the suite total.
Interoperation runs both ways. ReactCompat and OctaneCompat both come from the octane/react entry point: the first keeps real React components running inside Octane, the second drops compiled Octane components into a React app. The React compatibility guide walks through wiring up both compilers, rendering an Octane island inside a React tree, sharing React context across the boundary, and server rendering with hydration.
Be clear-eyed about the ecosystem. Octane ships first-party @octanejs/* ports of widely used React libraries, but how complete each one is varies: some match upstream behaviour, others are labelled partial or alpha, and the generated docs/bindings-status.md table is where you check what a package covers, which upstream version it tracks, where it diverges and whether SSR and hydration are handled. A curated set of first-party bindings is a different thing from the package ecosystem a React app draws on without thinking about it.
Octane is the most interesting answer yet to the question of what React’s programming model looks like with the runtime reconciler removed, and the two ergonomic wins are real enough to feel within an afternoon. Read the bindings status table for anything you depend on, then scaffold a throwaway SPA and put a hook inside a branch.
FAQs
Can I adopt Octane inside an existing React app without a rewrite?
Yes. The octane/react entry point exports OctaneCompat, which gives a compiled Octane subtree a home inside a real React 19 tree, so you move one screen, widget or component over at a time. Inside the island, use() or useContext reads the React contexts around it, events stay native, and server rendering works by importing the host from octane/react/server.
Does Context.Provider still work in Octane?
No. The 0.3.0 release dropped the legacy Context.Provider alias from client, server and native contexts, and the compiler now rejects statically recognised Provider access and tells you what to write instead. Use the context itself as the provider component and pass a value prop to it, or call createElement(Theme, { value }, children). The render-prop Consumer is gone too, and Octane's Differences from React page says it will not be added: slot-keyed hooks let use() or useContext run behind a condition, which is the problem Consumer existed to solve.
Are any hooks exempt from Octane's no-hooks-in-loops rule?
Yes. use() and useContext take no hook slot, so they are safe inside a plain JavaScript loop. Every slot-keyed hook would instead share one call site across all the iterations, which the compiler reports as an error. The documented ways round it are the keyed @for directive, which gives each item its own hook state, or moving the hook down into a child component.
Why does onChange behave differently in Octane than in React?
Octane uses real delegated DOM events with no synthetic event layer, so onChange is the browser's own change event: it fires when the edit is committed, usually on blur, rather than on every keystroke. Use onInput for per-edit updates. Controlled inputs still follow React's rules for value and checked, and refs are ordinary props rather than something passed through a wrapper object.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k