refactor(ui)!: migrate to React 19 ref-as-prop, drop forwardRef (#268) - #441
Merged
Conversation
Replace the deprecated React APIs flagged by react-doctor `no-react19-deprecated-apis` across packages/ui/src/components: - `forwardRef` -> ref-as-prop (ref passed as a normal function-component prop) - `useContext(X)` -> `use(X)` Bump the react/react-dom peer dependency to >=19.0.0 and regenerate the registry copies. Drives `no-react19-deprecated-apis` from 456 to 0 (measured with React 19 detection; the rule only runs on React 19+ projects). BREAKING CHANGE: @vllnt/ui now requires React 19 (peer react/react-dom >=19.0.0). Components use ref-as-prop, which does not work under React 18. Closes #268
|
ntk preview
Built from |
|
|
||
| if (shortcut) { | ||
| const tokens = shortcut.split(SHORTCUT_SEPARATOR).filter(Boolean); |
#268) - docs/agents/COMPONENTS.md: replace the forwardRef + displayName contract with the React 19 ref-as-prop + use() contract (React 19 now required). - scripts/migrate-react19.sh: re-runnable codemod (forwardRef -> ref-as-prop, useContext -> use) so newly added components — e.g. the #409-413 family PRs that still ship forwardRef — can be converted by re-running after they merge. Refs #268
pnpm-lock.yaml @vllnt/ui importer react/react-dom specifier >=18.0.0 -> >=19.0.0 to match the package.json peer bump; resolved versions unchanged (19.2.4). Fixes ERR_PNPM_OUTDATED_LOCKFILE on `pnpm install --frozen-lockfile` in CI.
This was referenced Jun 17, 2026
# Conflicts: # apps/registry/registry.json # apps/registry/registry/default/ai-sidebar/ai-sidebar.tsx # packages/ui/src/components/ai-sidebar/ai-sidebar.tsx
bntvllnt
added a commit
that referenced
this pull request
Jun 17, 2026
## What Clears the remaining react-doctor **State & Effects** warnings in `packages/ui/src/components` (issue #271), using React 19 patterns now available on the base branch. All changes are behavior-preserving and match the React 19 ref-as-prop base (no `forwardRef` reintroduced). ## Rules cleared (before -> after, scope: `packages/ui/src/components`) | Rule | Before | After | Fix | |---|---|---|---| | `prefer-use-effect-event` | 6 | 0 | wrap effect-captured callbacks in `useEffectEvent`, drop them from deps | | `no-derived-useState` | 4 | 0 | lazy initializer (`useState(() => prop)`) so prop-seeded state isn't a flagged stale copy | | `no-cascading-set-state` | 1 | 0 | collapse cascading `setState` into a `useReducer` | | `no-prop-callback-in-effect` | 1 | 0 | route the prop callback through `useEffectEvent` so a parent can't drive an update loop | | **Total** | **12** | **0** | | (`no-effect-event-handler` and `no-derived-state-effect` already had 0 in this scope on the base — the derived-state subset was cleared earlier by #387.) ## Files touched - `flow-diagram/flow-fullscreen.tsx`, `keyboard-shortcuts-help/`, `table-of-contents-panel/`, `slideshow/` — `useEffectEvent` for keydown handlers. - `search-bar/` — `onSearch` via `useEffectEvent` (+ regression tests). - `tldr-section/` — skeleton `useReducer` (+ regression test). - `date-picker/`, `number-ticker/`, `thinking-block/`, `spinner/unicode-spinner.tsx` — lazy initializers. - Regenerated `apps/registry/registry/default/*` (8 files) + `registry.json` via `registry:build`. ## Validation - `pnpm --filter @vllnt/ui build` (tsup ESM + DTS): pass - `tsc --noEmit --project tsconfig.build.json` (CI typecheck gate): pass - `pnpm --filter @vllnt/ui test:once`: 1385 tests pass (incl. 3 new regression tests) - `pnpm lint` (full `eslint .`): pass - `pnpm registry:build`: pass - react-doctor delta: 12 -> 0 target warnings, no new warnings introduced ## Stacking **STACKED on #441** (`refactor/react-19-migration-268`) — the remaining work needs React 19 `useEffectEvent`, which lives in #441, not yet on `main`. GitHub auto-retargets this PR's base to `main` when #441 merges. **Review/merge after #441.** Closes #271 --------- Co-authored-by: bntvllnt <bntvllnt@users.noreply.github.com>
bntvllnt
added a commit
that referenced
this pull request
Jun 27, 2026
Closes #432 Makes `main` ready to cut **stable 0.3.0** (per `docs/RELEASING.md`). **Docs + one test-guard tweak — this PR does not release.** Merge → dispatch Publish is the separate next step. > **Refreshed 2026-06-26:** rebuilt on top of current `main` (was 9 days stale / conflicting). Reconciled with everything that landed since it was authored — React 19 migration (#441), the core/forms/charts/motion families, and the count-derivation fix (#450). ## Changes - **Changelogs** (`CHANGELOG.md` + `packages/ui/CHANGELOG.md`): `[Unreleased]` → dated **`[0.3.0] - 2026-06-26`** (the publish workflow's release gate greps `## [0.3.0]`), dropped the now-satisfied pre-release notes, added the granular 0.3.0-train families (form primitives, charts/dataviz, AI/core primitives, motion/effects), a **Fixed** entry (theme-adaptive `CandlestickChart`/`SparklineGrid` colors), the `v0.3.0` link reference, and a fresh empty `[Unreleased]`. - **Component count → 309 everywhere.** The single source of truth is `registry.items.length` (= 309 after #450). Synced every prose surface that drifted: `README.md` (×3), `packages/ui/README.md`, `packages/ui/package.json` `"description"`, `docs/ARCHITECTURE.md`, and the ROADMAP. (The earlier 144→225 / 295 numbers are superseded.) - **ROADMAP**: status `planning` → `release prep`; `milestone TBD` → real 0.3.0 milestone; headline counts recomputed to **+169** net (140 → 309); the curated component list is relabeled as a highlight subset (it enumerates the first ~85, not all 169). - **RELEASING.md**: documents the `apps/registry` version bump, the full doc-sync step (now incl. `packages/ui/package.json` description + the count-guard test), and the **post-publish** `PUBLISHED_VERSION` flip. - **`apps/registry/lib/component-count.test.ts`** (added on `main` by #450): the guard asserted the count lives in `[Unreleased]`. Cutting the release moves it into `[0.3.0]` and empties `[Unreleased]`, which would break that guard. Retargeted it to assert the **newest** count snapshot in the changelog (first match, file is newest-first) — works both between releases and immediately after a cut. Still pins to `registry.items.length`. ## Deliberately NOT here - **No `PUBLISHED_VERSION` flip.** It stays `^0.2.1` until 0.3.0 is live on npm `latest`; flipping before publish would make the deployed registry advertise an unpublished version and break `npx shadcn add`. Documented post-publish follow-up. - No version bump — `packages/ui` and `apps/registry` are already `0.3.0`. ## Verification Count guard simulated against the real files (no local `node_modules` in the branch worktree; CI runs the real suite): - newest `CHANGELOG.md` count snapshot = **309** == `registry.items.length` - `home/{en,fr}.mdx` count claims all = **309** - PWA manifest derives from the registry (unchanged on `main`) ## Release sequence after this merges 1. Merge this PR (canary republishes `0.3.0-canary.<sha>`). 2. **Actions → Publish → Run workflow** on `main` → tags `v0.3.0`, publishes `--tag latest`, GitHub Release. 3. Post-publish PR: flip `PUBLISHED_VERSION` → `0.3.0` + regenerate registry (drift/integrity guards verify it). --------- Co-authored-by: bntvllnt <bntvllnt@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migrates
@vllnt/uioff the React APIs deprecated in React 19, eliminating everyno-react19-deprecated-apisreact-doctor warning in the library's canonical source (packages/ui/src/components).forwardRef→ ref-as-prop (React 19 passesrefas a normal function-component prop)useContext(X)→use(X)>=19.0.0(see Breaking change)ref-as-prop on function components only works in React 19, so dropping
forwardRefmakes React 19 a hard requirement. Thereact/react-dompeer range moves>=18.0.0→>=19.0.0. Copy-paste registry consumers on React 18 will get non-functional refs. The repo already targets React 19 everywhere else (registry app on^19.2.0,@types/react@19, test/build env on React 19.2.4), so this aligns the peer with reality.Warning count — rule
no-react19-deprecated-apis,packages/ui/src/componentsforwardRef+ 56useContext)How — re-runnable codemod
The deterministic 90% is a committed, re-runnable script:
scripts/migrate-react19.sh. It runs the two official codemods overpackages/ui/src/componentsand corrects the ref-prop type:npx codemod@latest run react-19-remove-forward-ref -t packages/ui/src/components npx codemod@latest run react-19-use-context-hook -t packages/ui/src/components # then: ref: React.RefObject<T> -> ref?: React.Ref<T> (optional, correct variance)Hand-fixed the cases the codemod cannot (documented in the script header): 3
forwardRef(NamedComponent)wrappers (file-upload,form,number-input), 1typeof forwardRef<…>alias (transaction-list), ~13 components whose intersection prop types the codemod dropped,ForwardedRefreceiver params widened for the now-optional ref, and 5max-lines-per-functionoverflows after reformatting. Removed a pre-existingascast inform.tsxthat only existed to undoforwardRef's generic erasure. Regeneratedapps/registrycopies viapnpm registry:build.🚨 Rebase + re-run after #409–413 merge
The parallel component-family PRs (#409–#413, ~73 new components) ship
forwardRef+displayName— they have to, to build green against the current React 18 workspace. They are not covered by this diff. After they merge, rebase this branch and re-runscripts/migrate-react19.sh(plus the documented hand-fix review) to convert the new components and reach a truly 0 count. Coordinate merge order with the team lead.Docs
docs/agents/COMPONENTS.md— the "forwardRef + displayName, no exceptions" contract is replaced with the React 19 ref-as-prop +use()contract.Validation (all run, all green)
pnpm --filter @vllnt/ui build(tsup ESM + DTS typecheck against React 19 types) — pass, 0 type errorspnpm --filter @vllnt/ui test:once— 1382 tests / 241 files passpnpm build(root; registry app Next.js build) — passeslinton changed files — cleanpnpm doctor→no-react19-deprecated-apis: 456 → 0No
as/@ts-ignore/anyintroduced;"use client"directives preserved; behavior-preserving. The 73 other react-doctor warnings (a11y / perf / bugs) are unrelated rules tracked separately in #271 / #277.Closes #268