InfernoJS v10.0.0
Inferno 10 moves work from the runtime to the compiler. The JSX plugins know the shape of an element's children when they compile it, so they now write it into the vNode flags. Inferno no longer computes it for every vNode at runtime. A vNode has one field less, delegated event handlers take less memory, and many allocations are gone from rendering, normalization and keyed diffing.
This release also rewrites inferno-animation, adds custom navigation confirmation to inferno-router, and fixes many bugs in core, hydration, server rendering, the router, compat and the MobX bindings.
Upgrading
-
Update every
inferno*package to 10. -
Update your JSX plugin to version 10:
Plugin Inferno 9 and older Inferno 10 babel-plugin-inferno7.x 10.x ts-plugin-inferno7.x 10.x swc-plugin-inferno3.x 10.x Version 10 of
babel-plugin-infernoandts-plugin-infernorequires Node.js 24 or newer to compile JSX.ts-plugin-infernodepends on TypeScript 6. -
Compile all your JSX again, including dependencies that ship precompiled JSX.
-
If you write your own
componentWillMoveoronComponentWillMovehooks, addimport 'inferno-animation'. See Move animation hooks. -
Compile your own components to ES2015 or newer. A component class compiled to an ES5 function can no longer extend
Component. See Browser support.
From version 10 on, the major version of each JSX plugin matches the major version of Inferno.
Each change is described in detail in the Migration guide.
Breaking changes
- JSX must be compiled by the v10 plugins.
VNodeFlagshave new values, so JSX compiled by an older plugin renders elements such as<svg>,<input>,<select>and<textarea>wrong. This includes npm packages that ship JSX compiled for Inferno 9. VNodeFlagshave new values. Code compiled withtscagainstinferno-vnode-flags9 has the old numbers inlined and must be compiled again. Never write flag numbers by hand.vNode.childFlagshas been removed. The shape of the children is stored in bits ofvNode.flags. Test them withVNodeFlags.HasKeyedChildrenand the otherHas*Childrenbits, or get the oldChildFlagsvalue with the newgetChildFlags(vNode)export.vNode.isValidatedhas been removed. It is the development-onlyVNodeFlags.Validatedbit now.$ReCreateandVNodeFlags.ReCreatehave been removed. Change the key of an element to re-create it. The v10 plugins report$ReCreateas a build error.- Delegated event handlers are stored on the element.
$EVis now a bitmask, and each handler is in its own property of the element, such as$onClick. This only matters to tools that read Inferno's internal DOM properties. - Move animation hooks have new semantics. See below.
- Modern bundles, and
Componentis a native class in all of them. See Browser support.
// v9
<div $ReCreate>{content}</div>
// v10: change the key whenever the element must be re-created
<div key={version}>{content}</div>Move animation hooks
The reconciler no longer runs move animations itself. inferno-animation installs a move engine when it is imported.
- Custom
componentWillMoveandonComponentWillMovehooks are only called when the app has importedinferno-animation. The exported animated components and helpers already import it. - The hooks are called for every item that a keyed update keeps, before anything is patched. In v9 they were called only for the items that were physically moved, after the list had already been partly changed.
- A class component's
componentWillMovemust exist by the end of its mount. A hook assigned later is not called. - Appear, leave and move hooks target the first element of the component. A component whose root is only text or empty gets no hook. In v9 the hook could get a text node, which crashed the helpers.
See the migration guide for details.
Browser support
All bundles are now compiled for Chrome 107, Edge 107, Firefox 84 (KaiOS 3) and Safari 16. In v9 the CommonJS and UMD bundles were compiled down to ES5 syntax. Now they keep modern syntax such as const, arrow functions, spread and classes, like the ES module bundles.
Component is a native class in every bundle. A component class that your compiler turns into an ES5 function, for example with TypeScript target: "ES5", throws TypeError: Class constructor Component cannot be invoked without 'new'. Compile your components to ES2015 or newer. See the migration guide for details.
Deprecations
create* vNode factories
newVNode, newComponentVNode, newTextVNode and newFragment replace createVNode, createComponentVNode, createTextVNode and createFragment. The new factories take the shape of the children as a bit in flags, so Inferno uses the flags as given. The v10 JSX plugins emit the new factories.
The old factories are marked @deprecated and keep working. They turn their childFlags argument into its bit and call the new factories.
import { newVNode, newFragment } from 'inferno';
import { VNodeFlags } from 'inferno-vnode-flags';
// v9: createVNode(VNodeFlags.HtmlElement, 'div', 'foo', 'text', ChildFlags.HasTextChildren)
newVNode(
VNodeFlags.HtmlElement | VNodeFlags.HasTextChildren,
'div',
'foo',
'text',
);
// v9: createFragment(children, ChildFlags.HasKeyedChildren, key)
newFragment(VNodeFlags.Fragment | VNodeFlags.HasKeyedChildren, children, key);When you copy flags from another vNode, clear the copied state first with flags & VNodeFlags.ClearOnCopy, then add the new child bit. See the migration guide for the full contract.
New features
Custom navigation confirmation in inferno-router
Browsers can suppress the native window.confirm dialog, for example iOS Safari when Back navigation triggers it. A suppressed dialog returns false, so the navigation stayed blocked. Router, BrowserRouter, HashRouter and MemoryRouter now accept a getUserConfirmation prop, so <Prompt> can use your own in-page dialog. Fixes #1699.
import {
BrowserRouter,
Prompt,
type GetUserConfirmation,
} from 'inferno-router';
const getUserConfirmation: GetUserConfirmation = (message, callback) => {
// Render your dialog and return a function that closes it
return showLeaveDialog({
message,
onLeave: () => callback(true),
onStay: () => callback(false),
});
};
<BrowserRouter getUserConfirmation={getUserConfirmation}>
<Prompt when={hasUnsavedChanges} message="Discard your changes?" />
{/* routes */}
</BrowserRouter>;The handler can answer synchronously or later. The cleanup function it returns runs once when the decision completes or is invalidated. Without the prop, Prompt keeps using window.confirm. See the inferno-router README for the details.
Rewritten inferno-animation
The public API is the same. (#1702)
- Items that are pushed aside by a moved item animate too, not only the moved ones.
- A move interrupted by another update continues from where it is on screen.
- When leaving items are removed, the remaining items slide into the gap.
- Offsets take 2D transforms of ancestors into account, also across shadow roots, as well as the SVG
viewBoxand the item's ownscaleandrotate. CSSzoom, perspective and rotation other than around z are not supported. - Nested lists move relative to an ancestor that starts moving in the same update.
- A leave that interrupts an enter starts from where the enter got to.
- Items with CSS keyframe or script animations move with the
translateproperty instead oftransform. - The app's inline styles are restored after an animation.
- Transitions with zero duration no longer leave items stuck.
- Transition handling shares two capture listeners per root (
transitionendandtransitioncancel) and groups fallback timers. - The move engine is dormant until a component with a move hook mounts. Apps that don't import
inferno-animationdon't pay for move support: bundlers drop the reconciler's checks. inferno-animation/index.cssis exported from the package.
Minified and gzipped, inferno-animation is 9.92 kB for the UMD bundle and 9.82 kB for the CommonJS bundle.
More boolean attributes
async, defer, disablePictureInPicture, disableRemotePlayback, formNoValidate, inert, itemScope, noModule and playsInline are now boolean attributes. Boolean attributes are written as attributes with their lowercase name, so both spellings work, for example readOnly and readonly or allowFullScreen and allowfullscreen. hidden and capture keep a string value such as hidden="until-found" or capture="user".
checked, indeterminate, muted and selected hold the current state of the element, so they are still set as properties.
autoFocus works on mount
Props are applied to an element before it is inserted into the document, so autoFocus focuses the element when it mounts.
TypeScript
createElementhas separate overloads for DOM elements, function andforwardRefcomponents, and class components, with improved support for callback and object refs.- Lifecycle hooks of function components may be
null. - A multiple
<select>acceptsnumber[]as its value. inferno-compat:render<T>()andunstable_renderSubtreeIntoContainer<T>()are generic in the returned instance;renderaccepts a callback;Children.*,createFactory,PropTypesandPureComponenthave accurate types; camelCase and numeric style objects andonDoubleClickare accepted.inferno-extras:findDOMNode()accepts any component instance,nullandundefined.inferno-mobx:inject()returns the actual injector type, exported asInjectedComponentandIInjector.observerWrapaccepts a typed context. Theobserver(stores)decorator returns the component.inferno-redux:connect()accepts all the options it forwards.- The
typescondition comes first in each package'sexports, so TypeScript no longer finds the types only through a fallback. - Published typings no longer import packages that are not dependencies.
Performance
- Smaller vNodes. Each vNode is 4 bytes smaller in Chrome and 32 bytes smaller in Firefox, where the object drops to a smaller size class.
- Child flags at compile time. The JSX plugins write the shape of the children into the flags as one number, so the runtime does no work to combine them.
- Delegated events. An element with one delegated handler takes 16 bytes less in Chrome. Unmounting an element visits only the events it registered, and event dispatch tests one bit before reading a handler.
- Keyed diffing. The key index is a
Map. Numeric keys such as row ids made a plain object fall back to a dictionary that was reallocated as it grew. This removes 45–50 KiB of garbage per "replace all rows" in js-framework-benchmark. - vNode reuse. A vNode referenced outside of render, such as a hoisted vNode, the root passed to
render()again or the root returned by a component, is no longer cloned when it is rendered again in the same position. Children that go through normalization are cloned only when needed, so a component that rendersprops.childrenno longer allocates new vNodes for them on every update. - Normalization. Index keys of children without a key are shared instead of being allocated as new strings on every render.
- Unmount. The unmount routine allocates less.
Bug fixes
Core
- A vNode used in two positions shared its DOM node and state between them, and only one position was updated afterwards.
- An element vNode with multiple children that was rendered in two places was updated in only one of them.
- A hoisted vNode with multiple children rendered the children of another vNode after an element of the same type was patched in its place.
- A component was unmounted and mounted again, losing its state, when the element rendered in its place was equal but had been normalized as the child of another element.
- Removing a Fragment with exactly one child did not unmount the child:
componentWillUnmountwas not called and refs were not cleared. - Re-rendering a Portal with a different container threw
Failed to execute 'removeChild' on 'Node'when its child was a component or a Fragment. - Reusing a vNode broke
dangerouslySetInnerHTML. - Changing
stylefrom a string to an object kept the declarations of the string. - An
<option>without avalueprop got the value"undefined". hiddenandcapturekept their string value when they changed totrue.- A
refpassed to aforwardRefcomponent through spread props was lost, and so were the lifecycle hooks that came with it. - Development errors named a
forwardRefcomponent"Object". displayNamenow takes priority over the function or class name in development messages.createElementpassedonComponentWillMoveas a prop instead of a hook.cloneVNode,createElementandhwrote into the props object passed to them.- Importing
infernoas a native ES module in a browser threw becauseprocessis not defined.
inferno-hydrate
- Hydration used a vNode that was already mounted elsewhere for the server-rendered DOM node, so one of the positions was not updated afterwards.
- Hydrating a Fragment with exactly one child broke the DOM.
- SVG elements with camelCase tag names, such as
<linearGradient>, were replaced instead of hydrated. - Appear hooks of components mounted after a mismatch were never called.
inferno-server
- A Fragment with exactly one child rendered an empty placeholder comment instead of its content.
- A Fragment with an empty children array and explicit child flags rendered nothing, which did not match the browser, and the next render after hydration threw.
- Rendering crashed when a component returned an array containing
null,undefinedor booleans. streamAsStringfailed andstreamQueueAsStringnever finished when the tree contained a number as text.RenderQueueStreamnever ended as a readable stream.renderToStringthrew for an<option>inside a Fragment.- Stream renderers did not mark the
<option>selected by the<select>value. Multiple select values and the<select>defaultValuewere ignored. defaultValueanddefaultCheckedwere rendered over a controlled value.- The value of a
<textarea>was rendered as an attribute instead of its content. dangerouslySetInnerHTML={undefined}crashed, andrenderToStringandstreamQueueAsStringrendered the children instead ofdangerouslySetInnerHTML.- Server renderers check a tag name before writing it, and throw an
Errorfor an invalid one instead of a string of HTML. componentWillMountwas called for a component withgetSnapshotBeforeUpdate, andgetChildContextwas called beforecomponentWillMount.streamAsStringrendered without props when the constructor did not pass them tosuper.streamQueueAsStringskippedgetDerivedStateFromPropsfor a component withgetInitialProps.- The UMD bundle threw when it was loaded in a browser.
inferno-router
Promptstopped blocking after the first confirmed transition.- In Chromium,
HashRouterlost Back navigation accepted through a synchronous confirmation, such aswindow.confirm, and left the next Back navigation unblocked. Redirectin aSwitchwent to itstopath without the matched params, andRedirectdropped thestateof a location object.Switchdropped the key of the matchedRouteand crashed onfalse,nullandundefinedchildren.Linkwithtarget="_self"loaded the page from the server.- Loaders:
- A loader that threw or returned synchronously stopped the navigation.
- A loader that resolved to
nullwas reported as an error. - After navigating, every matching route in a
Switchran its loader, not only the matched one. - The request passed to a loader had no query string.
- A
Routewithout apaththat had a loader never rendered. - Pending loaders were not aborted when the router unmounted.
StaticRouterstripped itsbasenamefrom paths that only start with it, such as/applicationwith the basename/app.- The UMD bundle could not find
historyandpath-to-regexp.
inferno-compat
PureComponentcrashed onsetStatewithout an initial state.fontVariantwas not mapped tofont-variant.- Props named like
Object.prototypemembers, such astoStringandconstructor, were mapped wrong.
inferno-mobx
observerdropped the arguments of patched lifecycle methods.observerWrapfailed when an observable change rendered another root element.observerPatchandobserverWrapkept reactions alive in server rendering. They now followuseStaticRendering.
inferno-redux
- The UMD bundle read Redux from the wrong global.
For contributors
- The repository uses pnpm instead of Lerna (#1698). Internal dependencies use
workspace:*, which is published as the exact version. - Browser tests run with Jasmine Browser Runner instead of Karma, in headless browsers locally (#1701).
- All tests are written in TypeScript (#1632).
- Differential fuzzers test vNode reuse in rendering and hydration.
Full list of changes: v9.1.0...v10.0.0