Skip to content

V2 cleanup: drop React 16/17 import vestiges (react-in-jsx-scope + scoped React.JSX) #322

Description

@CarlosNZ

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:

  1. Remove react/react-in-jsx-scope (both blocks) and the now-pointless React: 'readonly' global from eslint.config.mjs.
  2. Remove the ceremonial default import React from the files that only used it for JSX scope.
  3. Keep React imported (or switch to import { memo }) in the two React.memo files.
  4. 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.ElementReact.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:

  1. Replace JSX.Element with React.JSX.Element in types.ts and Icons.tsx.
  2. Drop the now-unused { type JSX } named imports.
  3. 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):

  1. chore: drop react-in-jsx-scope rule + remove ceremonial React imports
  2. 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

  • react/react-in-jsx-scope removed from eslint.config.mjs; ceremonial import React statements gone.
  • React.memo call sites still resolve React (default import or named memo).
  • JSX.ElementReact.JSX.Element; stray { type JSX } imports removed.
  • pnpm lint, pnpm compile, and pnpm test all green.

Metadata

Metadata

Assignees

No one assigned

    Labels

    V2To include in Version 2

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions