Context
The "Bump React peer to ≥18" roadmap item is done — the peer dep is react: ">=18.0.0". This issue tracks the two follow-on cleanups that the bump unlocks but which haven't been taken. Both are leftover React-16/17 compatibility patterns. Part of the V2 "Additional cleanup" section — umbrella #256.
Reality check / priority: on inspection the code is already mostly modern, so this is low-priority hygiene, not a fix for any breakage. pnpm compile is green today under the pinned @types/react@19.1.1. Worth doing while the v2 breaking-change window is open and the comment-cleanup pass (#292) is touching these files anyway, but nothing is broken.
Change 1 — Drop the react/react-in-jsx-scope rule + remove ceremonial import React
eslint.config.mjs sets react/react-in-jsx-scope: 'error' in both config blocks (comment: "Keep React in JSX scope for React 16 compatibility"), and declares React: 'readonly' as a global.
Why it exists: under the classic JSX transform (React ≤17 default), <div/> compiles to React.createElement('div'), so React had to be in lexical scope in every JSX file or it broke at runtime. The rule enforces import React from 'react' in all ~16 core .tsx files.
Why it's now redundant:
tsconfig.json is already on "jsx": "react-jsx" — the automatic runtime, which imports jsx() from react/jsx-runtime rather than calling React.createElement. JSX does not need React in scope.
@types/react does export = React; export as namespace React, so every React.* type reference (React.FC, React.CSSProperties, React.KeyboardEvent, React.ModifierKey, React.ReactNode, …) resolves via the ambient global namespace with no import at all.
The only genuine need for the default import is React used as a runtime value — and in core that is exclusively React.memo, in two files:
(All other React.memo hits are in comments.)
So with the rule off, the ~14 remaining default import React statements become unused imports (the existing no-unused-vars rule will flag them), and can be removed. The two React.memo files either keep the default import or switch to import { memo } from 'react'.
Work:
- Remove
react/react-in-jsx-scope (both blocks) and the now-pointless React: 'readonly' global from eslint.config.mjs.
- Remove the ceremonial default
import React from the files that only used it for JSX scope.
- Keep
React imported (or switch to import { memo }) in the two React.memo files.
pnpm lint && pnpm compile green.
Change 2 — Use the scoped React.JSX namespace
JSX.Element is used in ~18 spots — src/types.ts (10: icon overrides + custom-node children) and src/Icons.tsx (8: return-type annotations).
Correction to an earlier assumption: this is not relying on a soon-to-be-removed global JSX namespace. It is already explicitly imported — import { type JSX } from 'react' (types.ts:1), import React, { type JSX } from 'react' (Icons.tsx:1) — so it compiles fine and is already future-proof. Modern @types/react@19 has no global JSX namespace (only the legacy ts5.0/v18 compat shims do), which is exactly why the named import is needed today.
So this change is purely cosmetic / consistency: switch JSX.Element → React.JSX.Element, which lets the { type JSX } named import be dropped (since React.JSX comes free via the same ambient namespace as every other React.* type). It pairs naturally with Change 1's "rely on the ambient React namespace, stop importing ceremonially" theme. The official types-react-codemod scoped-jsx transform does exactly this if a codemod is preferred over hand edits.
If we'd rather keep the explicit import { type JSX } form (it's arguably just as clean), this change can be dropped — flag it on review.
Work:
- Replace
JSX.Element with React.JSX.Element in types.ts and Icons.tsx.
- Drop the now-unused
{ type JSX } named imports.
pnpm compile green.
Suggested PR shape
One PR, two commits — the changes are independent and don't conflict (both types.ts and Icons.tsx already reference React.* so React stays resolvable throughout):
chore: drop react-in-jsx-scope rule + remove ceremonial React imports
chore: use scoped React.JSX namespace
Out of scope (optional parallel follow-on)
packages/components has the same 14 import React files and its own lint config. Same treatment applies there but under that package's config — can be a separate commit/PR. Not required for this issue.
Acceptance criteria
Context
The "Bump React peer to ≥18" roadmap item is done — the peer dep is
react: ">=18.0.0". This issue tracks the two follow-on cleanups that the bump unlocks but which haven't been taken. Both are leftover React-16/17 compatibility patterns. Part of the V2 "Additional cleanup" section — umbrella #256.Change 1 — Drop the
react/react-in-jsx-scoperule + remove ceremonialimport Reacteslint.config.mjssetsreact/react-in-jsx-scope: 'error'in both config blocks (comment: "Keep React in JSX scope for React 16 compatibility"), and declaresReact: 'readonly'as a global.Why it exists: under the classic JSX transform (React ≤17 default),
<div/>compiles toReact.createElement('div'), soReacthad to be in lexical scope in every JSX file or it broke at runtime. The rule enforcesimport React from 'react'in all ~16 core.tsxfiles.Why it's now redundant:
tsconfig.jsonis already on"jsx": "react-jsx"— the automatic runtime, which importsjsx()fromreact/jsx-runtimerather than callingReact.createElement. JSX does not needReactin scope.@types/reactdoesexport = React; export as namespace React, so everyReact.*type reference (React.FC,React.CSSProperties,React.KeyboardEvent,React.ModifierKey,React.ReactNode, …) resolves via the ambient global namespace with no import at all.The only genuine need for the default import is
Reactused as a runtime value — and in core that is exclusivelyReact.memo, in two files:src/ValueNodeWrapper.tsx:575src/CollectionNode.tsx:692(All other
React.memohits are in comments.)So with the rule off, the ~14 remaining default
import Reactstatements become unused imports (the existingno-unused-varsrule will flag them), and can be removed. The twoReact.memofiles either keep the default import or switch toimport { memo } from 'react'.Work:
react/react-in-jsx-scope(both blocks) and the now-pointlessReact: 'readonly'global fromeslint.config.mjs.import Reactfrom the files that only used it for JSX scope.Reactimported (or switch toimport { memo }) in the twoReact.memofiles.pnpm lint && pnpm compilegreen.Change 2 — Use the scoped
React.JSXnamespaceJSX.Elementis used in ~18 spots —src/types.ts(10: icon overrides + custom-node children) andsrc/Icons.tsx(8: return-type annotations).Correction to an earlier assumption: this is not relying on a soon-to-be-removed global
JSXnamespace. It is already explicitly imported —import { type JSX } from 'react'(types.ts:1),import React, { type JSX } from 'react'(Icons.tsx:1) — so it compiles fine and is already future-proof. Modern@types/react@19has no globalJSXnamespace (only the legacyts5.0/v18compat shims do), which is exactly why the named import is needed today.So this change is purely cosmetic / consistency: switch
JSX.Element→React.JSX.Element, which lets the{ type JSX }named import be dropped (sinceReact.JSXcomes free via the same ambient namespace as every otherReact.*type). It pairs naturally with Change 1's "rely on the ambient React namespace, stop importing ceremonially" theme. The officialtypes-react-codemodscoped-jsxtransform does exactly this if a codemod is preferred over hand edits.Work:
JSX.ElementwithReact.JSX.Elementintypes.tsandIcons.tsx.{ type JSX }named imports.pnpm compilegreen.Suggested PR shape
One PR, two commits — the changes are independent and don't conflict (both
types.tsandIcons.tsxalready referenceReact.*soReactstays resolvable throughout):chore: drop react-in-jsx-scope rule + remove ceremonial React importschore: use scoped React.JSX namespaceOut of scope (optional parallel follow-on)
packages/componentshas the same 14import Reactfiles and its own lint config. Same treatment applies there but under that package's config — can be a separate commit/PR. Not required for this issue.Acceptance criteria
react/react-in-jsx-scoperemoved fromeslint.config.mjs; ceremonialimport Reactstatements gone.React.memocall sites still resolveReact(default import or namedmemo).JSX.Element→React.JSX.Element; stray{ type JSX }imports removed.pnpm lint,pnpm compile, andpnpm testall green.