What carries over—and what changes
Differences from React
Bring your React mental model. Octane keeps the component and hook APIs, then changes a small set of rules where the compiler or the browser can do more of the work.
Hooks fit the code
Octane tracks a hook by its source location, not the order in which hooks run. A hook can sit behind a condition or after an early return without shifting another hook's state:
export function Profile(props) {
if (!props.user) return <EmptyState />;
const [editing, setEditing] = useState(false);
return <button onClick={() => setEditing(!editing)}>Edit profile</button>;
}The exception is a slot-based hook inside a plain JavaScript loop. Every iteration
would share one source location, so the compiler reports an error. Use keyed @for or
move the hook into a child component. use() and useContext are exempt because they
are not slot-keyed.
Dependency lists are optional for effects, memos, callbacks, and imperative handles. When you omit one, the compiler derives it from the callback's captures:
useEffect(() => {
syncRoom(room.id);
}); // inferred from syncRoom and room.idLocally declared custom hooks in full-compiled .tsrx/.tsx modules participate when
they transparently forward their callback and final dependency parameter to one of
those hooks. The compiler follows nested transparent wrappers, but it does not guess
from a use* name alone. Plain .ts/.js modules, imported custom hooks, and wrappers
that transform or inspect those parameters need an explicit list.
An explicit array keeps its exact React meaning. Pass null when you intentionally
want the effect or computation to run after every render.
useState and useReducer also offer an optional third tuple item: a stable function
that reads the latest scheduled state. It replaces the ref often used after an
await:
const [draft, setDraft, getDraft] = useState('');
await waitForConnection();
await save(getDraft());Ordinary two-item destructuring stays on the normal two-item path.
Events come from the browser
Handlers receive the browser's real Event object, not a synthetic wrapper. Bubbling,
capture, stopPropagation(), and logical bubbling through portals still work. The
practical differences are:
- Use
onInputfor every text edit. Nativechangefires when the browser commits the edit, usually on blur. - There are no synthetic
onChange,onBeforeInput, oronSelectpolyfills. - Mouse and pointer enter/leave handlers use the platform's native events.
Controlled value and checked still behave like React: the prop drives the DOM and
is re-applied after renders and discrete events. Most text-field migrations are a
one-word change:
// React
<input value={text} onChange={(event) => setText(event.currentTarget.value)} />
// Octane
<input value={text} onInput={(event) => setText(event.currentTarget.value)} />The warning covers <textarea> and text-entry input types: a missing or invalid
type, plus text, search, url, tel, password, email, and number. It does
not apply to selects, checkbox/radio/file and other non-text input types, custom
elements, statically read-only/disabled controls, or component callbacks that happen
to be named onChange. The compiler surface uses warning severity; unresolved
final-prop violations report through console.error in development, once per broken
episode. An
onChangeCapture fix preserves the phase by using onInputCapture.
Native commit-on-blur behavior is valid. Mark it explicitly instead of adding a noop input handler:
<input
defaultValue={savedDraft}
onChange={(event) => save(event.currentTarget.value)}
suppressNativeChangeWarning
/>suppressNativeChangeWarning is a JS-only host hint. It suppresses only this
diagnostic, never appears in client or server HTML, and changes neither event
delivery nor controlled-state restoration.
Checkboxes and radios keep the browser's activation timeline: click, then
input, then non-cancelable change. Consequently, preventDefault() in native
onChange cannot roll the toggle back. Cancel the earlier onClick when rollback
is the intended behavior. React's synthetic checkable onChange is backed by the
cancelable click, so cancellation at that callback is an intentional timing
divergence even though Octane still restores rejected controlled state and radio
cousins after native change.
class and className compose clsx-style, so arrays, objects, and nested values work
without another helper:
<article class={['card', isActive && 'active', { selected }]} />React turns ['a', 'b'] into "a,b"; Octane produces "a b" on both client and
server.
Transitions without time slicing
Octane batches updates in a microtask, then runs each render to completion. There are urgent and transition updates, but no lanes, yield points, CPU time slicing, or selective hydration.
The useful transition behavior remains: when a slower replacement suspends, the
current screen can stay visible and isPending can explain the wait. flushSync
drains the full update queue, while passive effects still run after paint.
Octane always applies its compiler transform for avoidable use() waterfalls.
Requests it can prove independent can start together; a request that needs an
earlier result remains sequential:
const user = use(fetchUser(id));
const teams = use(fetchTeams(id)); // starts alongside fetchUserPromises created during render are safe in Octane — no cache() wrapper
needed. The compiler memoizes every creation that feeds a use() at its
declaration, keyed on its real inputs, and that includes local promise chains:
const userPromise = fetchUser(id);
const thumbnailPromise = userPromise.then((user) => user.thumbnail());
<Avatar thumbnail={use(thumbnailPromise)} />userPromise is keyed on id, and the derived .then link is keyed on
userPromise's identity — so nothing refetches on an unrelated re-render or a
suspense replay, the whole chain refetches exactly when id changes, and the
chain's head joins the prefetch (__warm) plan an ancestor can start early.
React re-runs an uncached creation on every render attempt (with a dev
warning), and the React Compiler stabilizes the promise identity but still
unwraps serially. In production output these memoized creations — and
useMemo/useCallback themselves — compile to inline caches: a dependency
hit costs a few comparisons with no closure, dependency-array, or hook-map
allocations.
Two known gaps are tracked while this area is under active work: independence
analysis stops at module boundaries (independent use() reads inside an
imported custom hook still serialize), and a transition-wrapped update
currently reveals newly-loaded content progressively — never rolling back —
where React holds the previous screen until the whole update completes.
Errors and server rendering
Octane has no class components, so error boundaries use a template block or the
function-based ErrorBoundary component:
@try {
<RiskyPanel />
} @catch (error, reset) {
<button onClick={reset}>Try again</button>
};An uncaught error is reported through console.error rather than React's
onUncaughtError callback.
Framework-authored errors in the core DOM client and server runtimes retain their complete messages in development. Optimized production builds replace those messages with Octane-owned, append-only codes and a link to the error decoder; Octane does not reuse React's error numbers. User-thrown errors and compiler diagnostics keep their original messages and codes.
Buffered server rendering returns { html, css }; the extra css field contains
Octane's scoped styles. Streaming still reveals Suspense content as it becomes ready.
During hydration, value mismatches are patched and structural mismatches are rebuilt
in place with a warning instead of being thrown to a boundary.
Less common observable differences
A same-value state update can skip Octane's component body where React may enter it once more before bailing out. The committed result is the same.
useSyncExternalStoredoes not repeat an unchanged snapshot read at commit just because the callback identity changed. Stores that notify subscribers are unaffected.When a form action rejects, Octane continues later queued actions instead of cancelling them.
The keyed reconciler uses a longest-increasing-subsequence algorithm. Final order, node identity, focus, and state match React; only the exact physical move pattern can differ.
Custom-element property handling stays closer to the browser. Applicable attribute diagnostics are expanding progressively from upstream behavior without embedding React's complete property-name table in every runtime.
APIs Octane leaves out
Octane is built around function components, hooks, and the DOM. It intentionally does not include:
- Class components, legacy roots, or class error-boundary lifecycles.
- Server Components, RSC/Flight, or
cache(). StrictModedouble-invocation,Profiler, orSuspenseList.forwardReforcreateRef. Refs are ordinary props, including callback refs, object refs, and arrays of refs.- Most
React.Childrenutilities. Keyed@foris the normal collection API.
useDebugValue exists for library compatibility but has no visible runtime effect.
Resource hints such as preload, preinit, preconnect, and prefetchDNS are
supported.