Skip to content

Coordinated upgrade: React 19 + react-router 7 + GraphiQL 5 #3408

Closed

Description

@mtrezza

Background

Dependabot has opened a cluster of major-version bumps that cannot be merged independently because they are tightly coupled through peer dependencies. GraphiQL v5 requires React 18/19 as a peer dependency, which forces the React 16 → 19 upgrade, which in turn pulls react-router into v7 territory. Merging any one of these in isolation would break npm install peer resolution and/or the runtime. This issue tracks the coordinated migration so the five PRs can land together on a dedicated branch.

Current versions (package.json on alpha):

Package Current Target
react 16.14.0 19.x
react-dom 16.14.0 19.x
react-router-dom 6.30.3 7.x (consolidated into react-router)
graphiql 2.0.8 5.x
graphql 16.12.0 16.x (compatible, no change required)
react-test-renderer (dev) 16.13.1 19.x (deprecated upstream)

Toolchain is already modern and does not block the work: webpack 5, Babel 7 (@babel/preset-react 7.28.x, new JSX transform available), Jest 30 + jsdom, Node engines >=20.19.

Deferred Dependabot PRs

These five PRs are intentionally left open and were deferred pending this coordinated migration. They should be superseded by (or rebased into) the migration branch rather than merged one at a time:

Affected areas (from source audit)

React mount points (all use the removed ReactDOM.render) — must migrate to createRoot:

  • src/dashboard/index.js:22 — main dashboard SPA entry
  • src/quickstart/index.js:16 — quickstart entry
  • src/parse-interface-guide/index.js:14 — Parse Interface Guide (PIG) entry

react-router — already on clean v6 idioms (no Switch/Redirect/useHistory leftovers), so this is a v6 → v7 move, not a v5 → v6 rewrite:

  • src/dashboard/Dashboard.js:53 — central BrowserRouter + nested Routes/Route/Navigate/Outlet tree (~109 Route usages, includes splat routes such as cloud_code/*)
  • src/parse-interface-guide/routes.js — BrowserRouter/Routes/Route
  • src/lib/withRouter.js — custom HOC bridging v6 hooks (useParams/useNavigate/useOutletContext/useLocation) into class components; ~60 components consume it
  • Other react-router-dom importers: src/dashboard/AppData.react.js, src/dashboard/Settings/GeneralSettings.react.js, src/dashboard/Push/PushDetails.react.js, src/dashboard/Data/Playground/Playground.react.js (useBeforeUnload), src/dashboard/Data/Browser/Browser.react.js (useBeforeUnload), src/dashboard/Data/Jobs/JobsData.react.js, src/dashboard/Apps/AppsIndex.react.js, src/dashboard/Data/ApiConsole/ApiConsole.react.js, src/components/Sidebar/{SidebarSubItem,AppsMenu,SidebarHeader,SidebarSection}.react.js, src/components/FileTree/FileTree.react.js, src/components/Toolbar/Toolbar.react.js (useNavigate/useNavigationType/NavigationType), src/components/CategoryList/CategoryList.react.js, src/parse-interface-guide/PIG.react.js (NavLink)

GraphiQL — single consumer, narrow blast radius but a large library jump:

  • src/dashboard/Data/ApiConsole/GraphQLConsole.react.js — import GraphiQL from 'graphiql'; class component rendering <GraphiQL headers={...} headerEditorEnabled={true} fetcher={async (params, { headers }) => fetch(...)} />
  • src/dashboard/index.js:18 — require('graphiql/graphiql.min.css') (CSS import path changes in v5)
  • src/dashboard/Dashboard.js:279 — <Route path="graphql" element={<GraphQLConsole />} />

Legacy React patterns audit (what is / is not a blocker):

  • String refs: none ✔
  • findDOMNode: none ✔
  • defaultProps/propTypes on function components: none (no X.defaultProps = assignments; the app is ~148 class components, where defaultProps remains supported) ✔
  • enzyme / create-react-class / mixins: not used ✔
  • createPortal: 2 usages (src/components/Popover/Popover.react.js, src/dashboard/Data/CustomDashboard/elements/ExpandModal.react.js) — still supported, no change ✔
  • Legacy lifecycles (componentWillMount in ~30 files, componentWillReceiveProps in ~33 files, componentWillUpdate in 1) — not removed in React 19, but they emit warnings and are unsafe under StrictMode/concurrent rendering. Non-blocking; recommend incremental cleanup.
  • react-test-renderer used in 3 tests (BrowserRow, ConfigConflictDiff, ScriptResponseModal) — deprecated in React 19; must at minimum bump to 19, ideally migrate to @testing-library/react.

Per-library required changes

React 16 → 19

  • Replace ReactDOM.render(...) with createRoot(container).render(...) in the three entry files listed above (the 16 → 18 step; render is removed in 19).
  • Bump react, react-dom, and dev react-test-renderer to 19; audit for the React 19 removals (ReactDOM.render/hydrate/unmountComponentAtNode, string refs, legacy Context, findDOMNode, react-dom/test-utils) — only ReactDOM.render applies here.
  • New JSX transform is mandatory in 19 (already available via @babel/preset-react; confirm it is enabled).
  • ref becomes a regular prop and element.ref is deprecated; useRef now requires an argument. Sweep for bare useRef() calls.
  • Error handling changed (uncaught errors are no longer re-thrown; onUncaughtError/onCaughtError on the root). Verify error boundaries and any global handlers.
  • Expect StrictMode double-invocation and legacy-lifecycle deprecation warnings in dev.

react-router 6 → 7

  • Enable the six v6 future flags first and fix fallout before bumping: v7_relativeSplatPath (relevant — we have splat routes), v7_startTransition, v7_fetcherPersist, v7_normalizeFormMethod, v7_partialHydration, v7_skipActionErrorRevalidation. With all flags on, v7 is a non-breaking bump.
  • Package consolidation: remove react-router-dom, add react-router@7; rewrite from 'react-router-dom' → from 'react-router' across the ~18 files above and src/lib/withRouter.js. (RouterProvider/DOM-only exports move to react-router/dom; not used here.)
  • All component/hook APIs in use (BrowserRouter, Routes, Route, Outlet, Link, NavLink, Navigate, useNavigate, useParams, useLocation, useOutletContext, useBeforeUnload, useNavigationType/NavigationType) continue to work unchanged. Node 20+ requirement is already met.

GraphiQL 2 → 5

  • v5 is ESM-only, requires React 18/19 as a peer (the reason for the coupling), and splits into @graphiql/react plus plugin packages.
  • Editor moved from CodeMirror to Monaco: the project currently ships no Monaco/CodeMirror config, so the webpack build must be updated to provide Monaco web workers (monaco-editor worker setup). This is the largest build-config task in the migration.
  • CSS import path changes: graphiql/graphiql.min.css → graphiql/style.css (update src/dashboard/index.js:18).
  • Component API changes in GraphQLConsole.react.js: legacy props (query/variables/headers/response/readOnly) are removed in favor of initialQuery/initialVariables/initialHeaders; headerEditorEnabled no longer applies. Adopt createGraphiQLFetcher from @graphiql/toolkit for the fetcher (the existing custom fetch fetcher must be re-expressed against the v5 API). Verify the import shape (default vs named export) against v5.

Recommended sequencing

Land as one branch (e.g. chore/react19-router7-graphiql5) but commit in this order so each step is independently reviewable and bisectable:

  1. React 16 → 18 — convert the three entry points to createRoot, adopt automatic batching, run the suite under React 18 StrictMode. This is the highest-value, lowest-risk step and unblocks everything else. Rationale: 17 is a near-transparent stepping stone; 18's createRoot is the real gate.
  2. React 18 → 19 — apply the 19 removals sweep, bump react-test-renderer to 19 (or begin the @testing-library/react migration), fix useRef/ref-as-prop and error-handling changes. Rationale: keep the pure-React churn isolated from routing/GraphiQL churn.
  3. react-router 6 → 7 — enable the six future flags on alpha (ideally as a separate earlier PR to de-risk), fix splat-path/relative-link fallout, then flip imports to react-router and remove react-router-dom. Rationale: with flags pre-adopted this becomes mechanical and won't entangle the React diff.
  4. GraphiQL 2 → 5 — done last because it depends on React 18/19 being in place and carries the heaviest build work (Monaco workers, CSS path, rewritten fetcher/props). Rationale: smallest surface (one component) but the most environment-sensitive; isolating it last keeps the earlier steps green.

Risk areas

  • Monaco worker bundling under webpack 5 — misconfiguration commonly yields blank editors or worker 404s at runtime, not build failures. Highest technical risk.
  • react-test-renderer deprecation — snapshot tests in BrowserRow, ConfigConflictDiff, ScriptResponseModal may warn or break under React 19.
  • StrictMode double-invocation exposing side effects in the ~65 legacy lifecycle methods (data browser, push, analytics screens).
  • v7_relativeSplatPath altering resolution of the splat routes (cloud_code/*, PIG path="*") and any relative links beneath them.
  • withRouter HOC — the single bridge for ~60 class components; a regression here has wide reach.
  • GraphiQL fetcher/headers rewrite — master-key/client-key header injection must be preserved exactly for the GraphQL console to authenticate.
  • Third-party peer warnings from other React-consuming deps once React hits 19.

Manual QA checklist (dashboard UI)

  • App boots at / and quickstart/PIG bundles load without console errors
  • App selector, sidebar navigation, and deep links / browser back-forward behave (router)
  • Data Browser: load class, filter, sort, edit cell, add/delete row/column, relations, export/import
  • Cloud Code, Jobs (schedule/run), Logs, Config, Webhooks, Views screens render and route
  • Push: audiences, new push, activity screens
  • Analytics: overview, explorer, retention, performance, slow queries
  • API Console → REST console works
  • API Console → GraphQL console: editor mounts (Monaco), schema loads, a query runs against a configured graphQLServerURL, master/client-key headers are sent, header editor behaves
  • useBeforeUnload prompts still fire on unsaved changes (Browser filters, Playground)
  • Modals/popovers (createPortal) render and dismiss correctly
  • Full npm test, npm run lint, and production npm run build are green

Rough effort estimate

Roughly 5–8 engineering days for one engineer familiar with the codebase:

  • React 16 → 18 (createRoot + smoke): ~1 day
  • React 18 → 19 (removals sweep, tests): ~1–1.5 days
  • react-router 6 → 7 (flags + import consolidation + splat fixes): ~1–1.5 days
  • GraphiQL 2 → 5 (Monaco worker webpack config, CSS, fetcher/props rewrite, QA): ~2–3 days
  • Coordinated regression QA across all dashboard screens: ~1 day

Treat GraphiQL/Monaco as the schedule driver; the rest is comparatively mechanical.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions