Skip to content

Upgrade to React 19, Chakra v3, stac-react v1#72

Draft
alukach wants to merge 24 commits into
mainfrom
chore/react-19-upgrade
Draft

Upgrade to React 19, Chakra v3, stac-react v1#72
alukach wants to merge 24 commits into
mainfrom
chore/react-19-upgrade

Conversation

@alukach
Copy link
Copy Markdown
Member

@alukach alukach commented May 25, 2026

Important

This upgrade was AI-driven. The code changes — dep bumps, theme/recipe rewrite, component sweep, test wrapper updates, bug fixes — were generated by Claude Code (Opus 4.7) under human direction. Each step was reviewed at the diff level and the running app was manually smoke-tested against the dev server throughout. Automated gates (tsc, jest, lint, parcel build) all pass; visual/runtime parity against main was verified side-by-side on the collection list, collection detail, item detail, and form pages. Treat this PR as machine-generated code that has been read, exercised, and signed off on — not as untouched LLM output.

Summary

Upgrades stac-manager to the current major versions of its core UI and data dependencies:

Dep Was Now
react / react-dom 18.3 19.2
@chakra-ui/react 2.8 3.35
@developmentseed/stac-react 0.1.0-alpha.10 1.0.0-alpha.3
@devseed-ui/collecticons-chakra 3.0 4.0
framer-motion 10.16 12.40

The upgrade was an interlocked set: every one of these libraries had peer-dep constraints that pulled the others along. stac-react ^1.0.0-alpha.3 peer-requires React 19. Chakra v2 peers refuse React 19, so we needed Chakra v3. collecticons-chakra v3 peers on Chakra v2 + framer-motion v10, so we needed collecticons-chakra v4. Framer Motion v10 peers on React 18, so it had to move to v12. Once you pick one, you pick them all.

End state: a single React 19 across the tree, a single framer-motion 12, theme + components on Chakra v3 idioms, and adoption of stac-react v1's improved API (typed isLoading instead of state enum, callable-options for token rotation).

What that actually involved

Mechanical migration in order of landed commits:

  • Deps & lockfile — bump in lockstep so the tree resolves cleanly without --legacy-peer-deps / overrides.
  • Provider + themeextendThemecreateSystem + defineConfig. Custom palettes (primary, base, danger, …) participate in Chakra v3's colorPalette semantic-token system; neutrals (base, gray, surface) use the gray-style mapping (lighter border/muted), chromatic palettes use the standard. Recipe overrides for button, card, field, menu, select with full anatomy slot lists. ColorModeScript dropped (no light/dark mode in use).
  • Component sweep — namespace migrations (Card.Root/Header/…, Menu.Trigger/Content/Item, Tag.Root/Label, Avatar.Root/Image/Fallback, Field.Root/Label/ErrorText, RadioGroup, Table, Popover, List), prop renames (colorSchemecolorPalette, isAttachedattached, noOfLineslineClamp, spacinggap, isDisableddisabled), removed-export replacements (DividerSeparator, MenuButton/MenuListMenu.Trigger/Content, useToastcreateToaster+<Toaster />, Input*ElementInputGroup startElement/endElement, useDisclosure().isOpen.open, forwardRef from react, ChakraPropsHTMLChakraProps).
  • stac-react v1 adoptionuseCollection/useItem/useStacSearch consumers updated for the new hook shape. useCollections rewritten to consume the shared authed Api via useApi() instead of duplicating token-options wiring (the bridge in main.tsx is the single source). Added AbortController and dropped a fake hand-rolled debounce that wasn't actually debouncing.
  • Tests — wrappers on <ChakraProvider value={defaultSystem}>. Snapshots re-baselined. structuredClone polyfill added to jest-setup.ts (Chakra v3 needs it; jsdom@20 doesn't ship it).
  • Bug-fix sweep — Rules-of-Hooks fixes (hooks called after conditional throw), JsonEditor stale-closure fix via ref-stash, restored visual parity (Card padding, Heading size mapping, Tag colors, Login button colorPalette, semantic-token border shades), CollectionDetail items-list fetch on direct page load (stac-react v1's internal state-reset on StacApi rebuild was wiping collections mid-init).
  • Cleanup — deleted 9 unreferenced dead files (ItemForm, PropertyList/TableValue/Roles, Pagination + usePaginateHook, Callback, usePrevious) and duplicate type defs. forwardRef boilerplate dropped from 11 pure pass-through wrappers (React 19 ref-as-prop). Old commented-out v2 JSX blocks removed.

Gates

  • Tests: 113/113 passing ✅ (matches main; lower than peak 127 due to dead usePaginateHook.test.ts removal)
  • tsc: clean across client, data-core, data-widgets, data-plugins
  • lint: 0 errors (20 pre-existing no-explicit-any warnings only) ✅
  • parcel build: succeeds ✅
  • dev server: serves valid HTML ✅

Visual / runtime smoke test before merge

Automated gates can't verify Chakra v3's structurally-different DOM looks right or that namespace APIs route events correctly. Recommended walk-through:

  • Browse the collection list — cards render with thumbnails (or the SVG placeholder when no image), tags have the brand-tinted background, headings/body match expected sizes
  • Click into a collection — items load on direct page load (regression we found + fixed: stac-react v1 useStacSearch resets internal state when the underlying StacApi rebuilds during OIDC token load)
  • Click into an item — map renders, COG preview overlay loads within the item's bbox (out-of-bbox tile errors stop firing)
  • Open the user menu — Avatar sits inside the Logout button, no DOM-nesting warnings in console
  • Trigger a form submit error in CollectionForm — toast appears bottom-right with the correct indicator icon
  • Trigger a 400 validation error from CollectionForm — bell icon shows badge count
  • Edit form widgets render correctly (inputs, selects, radios, checkboxes, tagger, json editor, nested arrays/objects)

Companion PR

@devseed-ui/collecticons-chakra@4.0.0 is already on npm and is what this branch consumes. developmentseed/collecticons#30 in the collecticons repo lands two minor cleanups noticed while preparing this consumer (nested-svg in the factory, duplicate deps/peerDeps) — not blocking, land at leisure.

Plan doc

Migration plan with task-level detail (now mostly of historical value): docs/plans/2026-05-25-react-19-chakra-v3-migration.md.

Out of scope (deferred to follow-ups)

  • Perf hotspotsPluginBox rebuilds plugin.editSchema(values) on every keystroke; useRenderKey fights FastField with deep-compare + remount; usePlugins re-resolves on every data change. Real perf wins available; don't fit in this PR.
  • A11y polish — missing label associations on CollectionList search/filter, missing alt on a few <Image>/<Avatar.Image>, NotificationButton disclosure semantics, "Spacial extent" typo.
  • ItemMap COG preview — tile service returning HTTP 204 for empty areas; maplibre logs decode errors on the empty body. Pre-existing; not migration-caused.

alukach and others added 20 commits May 25, 2026 11:31
…akra 4

Cross-cutting dep bump in preparation for the v3 + 19 migration. Code
still uses Chakra v2 APIs so tests fail — addressed in subsequent
commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- extendTheme → createSystem + defineConfig
- ChakraProvider theme={} → ChakraProvider value={}
- styles.global → config.globalCss
- component overrides → recipes / slotRecipes
- Drop ColorModeScript (no light/dark mode in use)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename props: colorScheme→colorPalette, isAttached→attached,
  noOfLines→lineClamp, spacing→gap, isDisabled→disabled
- Restructure namespace components: Card, Menu, Table, Avatar, Tag,
  RadioGroup, ProgressCircle, Popover, List
- Replace removed components: Divider→Separator,
  FormControl/Label/ErrorMessage→Field.*, Input*Element→InputGroup
  start/endElement, Radio→RadioGroup.Item, CircularProgress→
  ProgressCircle.*, UnorderedList/ListItem→List.*, MenuButton/List→
  Menu.Trigger/Content, Select→NativeSelect.*
- forwardRef now imported from react
- ChakraProps → HTMLChakraProps
- useDisclosure().isOpen → .open
- Fade → framer-motion (motion.div + AnimatePresence)
- variant="link" not used; soft-outline cast to 'outline' to satisfy
  v3 button recipe types until variant typing is augmented properly
- Drop leftIcon/rightIcon on Button/IconButton; render icons as
  child nodes
- Update useItem/useCollection consumers: hook now exposes
  isLoading instead of state
- Drop dead pages/ItemList tree (was importing missing modules and
  not wired into router)
- Fix rollup-plugin-typescript2 include glob (its default pattern
  `*.ts+(|x)` no longer matches under current pluginutils, blocking
  all subpackage builds)

Note: useToast call sites in pages/CollectionForm/index.tsx and
components/Notifications.tsx are pointed at a tiny local shim
(src/_legacy/useToast.ts) so parcel can resolve the import while
B-7 rewires the v3 toaster singleton + <Toaster /> mount. The
shim's methods are no-ops; toasts will silently no-op until B-7.

The 4 remaining tsc errors are in test files (ChakraProvider now
requires a `value` prop). Those belong to B-8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace v2 useToast hook with v3 createToaster singleton +
  <Toaster /> mount in main.tsx
- Custom NotificationBox rendering moved from per-toast render
  callback to the <Toaster /> render function, switched on
  meta.kind
- Standard toasts (CollectionForm submit/update/dismiss) mapped
  status->type, close->dismiss, closeAll->dismiss()
- Delete the temporary _legacy/useToast shim

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…line snapshots

- Pass `value={defaultSystem}` to all ChakraProvider test wrappers
  (Chakra v3 requires it)
- Re-baseline plugin-box and array-fieldset snapshots tied to Chakra v3's
  DOM emission (class name churn, removal of Box's css-0 wrapper divs,
  Button no longer wraps icons in chakra-button__icon span)
- Polyfill globalThis.structuredClone in jest-setup.ts so Chakra v3's
  recipe internals can run under jest-environment-jsdom@29 (jsdom 20)
- Rework data-widgets/utils.test.ts to snapshot rendered DOM instead of
  the raw React element (pretty-format@29's ReactElement plugin doesn't
  recognise React 19's Symbol(react.transitional.element))

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CollectionDetail: restore total-count badge by reading numberMatched
  off the untyped STAC response (stac-react v1 dropped the field from
  its typed signature but most servers still emit it); hide badge when
  unavailable instead of showing misleading page-count
- UserInfo: wrap Avatar.Root with asChild + span to avoid invalid
  div-inside-button DOM nesting
- CollectionForm: preserve structured error.detail via JSON.stringify
  fallback instead of String() which produced "[object Object]"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address remaining lint errors after the Chakra v2 → v3 / React 19 sweep:
- Add explicit displayName to forwardRef'd components in client, data-core,
  and data-widgets (react/display-name)
- Drop now-meaningless eslint-disable comments whose target rules are no
  longer registered (react-hooks/exhaustive-deps) or no longer firing
  (@typescript-eslint/no-non-null-assertion)
- Extract NativeSelect onChange handler to avoid jsx-curly-newline /
  prettier circular conflict
- Replace `children:` prop literal in utils.test.ts createElement call
  with a typed ComponentProps variable to satisfy both react/no-children-prop
  and the React 19 ChakraProvider TS overload
- Auto-applied prettier formatting across the affected files

No production logic changed; lint clean across all 4 packages,
109/109 tests passing, build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mount-only useEffect captured onChange/onLoad from the first render
and wired them into the JSONEditor instance. If a parent re-renders
with a new callback identity, edits would route to the stale closure.
Route callbacks through refs that are updated every render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WidgetSelect and usePaginateHook called hooks after `throw` statements,
violating the Rules of Hooks. If a transition between throwing and
non-throwing inputs occurred without an unmount, React would crash
with "Rendered more hooks than during the previous render". React 19's
stricter dev-mode checks will flag this.

WidgetSelect: extracted an inner component so the outer one does the
prop validation (throws) and the inner one calls hooks unconditionally.
usePaginateHook: hooks moved above throws. The accompanying test cases
that previously asserted throws by calling the hook bare now use
renderHook so the hook runs inside a proper React render context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Avatar.Root asChild<span> wrapping defeated Avatar's internal slot
context. Chosen approach: render the Avatar as a sibling of the Logout
Button inside a Flex container, so the Avatar keeps its native <div>
root and the Button keeps native <button> semantics — no div-inside-
button DOM violation, slot context intact, keyboard accessibility
preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- RequireAuth: colorScheme='primary' was a v2 leftover; v3 wants
  colorPalette. The Login button now picks up the primary palette.
- WidgetRadio: copy-paste error in the allowOther validation error
  message referenced WidgetCheckbox; fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ndicator

- Slot recipes (menu/select/field) now declare the full v3 anatomy
  via {menu,select,field}Anatomy.keys() instead of partial lists. The
  partial-list trap silently strips styling from omitted slots — same
  pattern we hit and fixed for `card` earlier.
- ButtonGroup attached-button negative-margin rules now target the v3
  Group [data-attached] attribute selector instead of the dead v2
  `.chakra-button__group` class. The 2px overlap on attached outline
  buttons now actually renders.
- <Toast.Indicator /> mounted in the Toaster render function so
  type='loading' / 'success' / 'error' / 'warning' / 'info' actually
  display their indicator (v3 no longer auto-renders the icon).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…auth wiring

- Replace the hand-rolled `debounce` (recreated a fresh closure per
  call so it never debounced) with a straight AbortController-gated
  fetch. The previous implementation also guarded the fetch effect on
  `!collections`, so offset/limit changes never refetched once data
  had loaded — fixed by depending on offset/limit directly and aborting
  the in-flight request on dep change or unmount.
- Read the configured Api via useApi() instead of re-deriving an
  Authorization header from useAuth().token. The StacApiAuthBridge in
  main.tsx is the single source for auth-to-API wiring; this hook now
  consumes it like the rest of the app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…variant, drop fontSize='md' on Text

These previously-uncommitted fixes from the in-flight visual-regression
sweep were left in the working tree; landing them now.

- SmartLink: the v3 asChild path was `<ChLink asChild><Link to={to}/></ChLink>`,
  where the self-closing Link had no children and Chakra v3's Slot wiring
  doesn't merge children from the wrapper. Result: every collection card
  rendered an empty `<a class="chakra-link"></a>`. Destructure children
  out and pass them through both branches.
- ItemCard: B-4 sweep changed `variant='filled'` -> `variant='subtle'`,
  but the theme still defines a custom `filled` variant with
  `background: 'base.50'`. Restore with a cast (Chakra v3 type only
  knows about default variants).
- CollectionDetail / ItemDetail / ItemCard: B-4 mechanically converted
  v2's `<Text size='md'>` (silently ignored in v2) to v3's `<Text
  fontSize='md'>` (actively 20px). v2 rendered these at inherited body
  size (16px). Drop the prop so they inherit again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- MainNavigation: List.Root needed explicit flexDirection='row'. The
  v3 list recipe defaults to flexDirection: column, so passing only
  display='flex' kept the Browse/Create items stacked vertically.
- UserInfo: restore the v2 visual (Avatar inset within the Logout
  button) instead of the sibling-Flex restructure. Use Avatar.Root
  asChild + <span> so the Avatar renders as phrasing content (legal
  inside <button>); the recipe context + Ark UI state machine still
  apply because withProvider provides them independently of the
  rendered tag (verified empirically — Image gets data-state="hidden",
  Fallback "visible"). Avatar sized via size='xs' (32×32, 12px font),
  Button pl='2px' to keep the leading-avatar layout tight.
- CollectionDetail: items now fetch on direct page load. stac-react
  v1's useStacSearch internally calls M() (state reset) whenever its
  StacApi instance changes, which fires during the OIDC token-load
  remount on direct loads. The previous effect deps ([collectionId,
  setCollections]) were stable, so the reset wiped `collections`
  without ever being restored, leaving the query disabled and no
  /search request firing. New effect depends on `collections` and
  re-applies [collectionId] whenever it gets cleared.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files deleted (verified no external imports):
- pages/ItemForm/* — unrouted page importing non-existent components
- pages/ItemDetail/{PropertyList,TableValue,Roles}.tsx — no consumers
- components/Pagination.tsx — only referenced in commented-out blocks
- utils/usePaginateHook.{ts,test.ts} — only used by deleted Pagination
- components/auth/Callback.tsx — OIDC callback handled inline in auth/Context
- hooks/usePrevious.tsx — re-exported but never imported anywhere

Also dropped from types/index.ts:
- LoadingState (still locally redefined where actually needed)
- Property / PropertyGroup / PropertyItem (only used by deleted PropertyList)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hand

- data-core: widget-renderer.tsx had a local WidgetProps interface
  identical to the exported one in ./types. Replace with an import so
  external consumers and the renderer itself share one definition.
- pages/{CollectionList,CollectionDetail,ItemDetail}: remove
  commented-out v2 JSX blocks (search-params pagination, old Add-new
  / Edit Button placeholders with v2 colorScheme + leftIcon). These
  referenced symbols that no longer exist after the migration or were
  marked for follow-up that's now resolved differently.
- components/Notifications.tsx: split `borderBottom='1px solid'` +
  `borderColor='base.100'` into individual border-bottom-{width,style,
  color} props. The shorthand was resetting all four sides via CSS
  cascade order; same trap we fixed in the theme's button/input recipes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
React 19's ref-as-prop simplification makes forwardRef + manual
displayName ceremony unnecessary for components that don't take refs
externally. Audited: no consumer passes ref to any of these.

- data-widgets/lib/components/elements.tsx: Fieldset, FieldsetHeader,
  FieldsetBody, FieldsetFooter, FieldLabel, FieldIconBtn,
  FieldsetDeleteBtn — 7 plain pass-through wrappers, now plain function
  components. Removed 7 displayName assignments (function name suffices).
- data-core/lib/components/error-box.tsx: ErrorBox same treatment.
- client/src/components/auth/ButtonWithAuth.tsx,
  MenuItemWithAuth.tsx: same.
- client/src/components/InnerPageHeader.tsx: InnerPageHeader accepts
  `ref` as a regular prop (consumed by InnerPageHeaderSticky for its
  IntersectionObserver). InnerPageHeaderSticky drops the merged-ref
  dance since no external consumer ever passed it a ref — just owns
  the local ref directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The .env file slipped into 5aa4a44 as part of the dead-file removal
commit (untracked → staged → committed alongside the intentional
deletions). Nothing sensitive was in it, but it shouldn't be in the
repo. Removing from tracking and adding to .gitignore so future
.env / .env.local / .env.*.local don't accidentally land again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alukach alukach requested review from AliceR and danielfdsilva May 26, 2026 05:25
@alukach alukach changed the title Migrate to React 19 + Chakra UI v3 Upgrade to React 19, Chakra v3, stac-react v1 May 26, 2026
@alukach alukach marked this pull request as ready for review May 26, 2026 05:27
@alukach alukach marked this pull request as draft May 26, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant