Repository navigation
Coordinated upgrade: React 19 + react-router 7 + GraphiQL 5 #3408
Description
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:
- refactor: Bump react-dom from 16.14.0 to 19.2.5 #3336 — Bump
react-domfrom 16.14.0 to 19.2.5 - refactor: Bump react-router-dom from 6.30.3 to 7.14.1 #3341 — Bump
react-router-domfrom 6.30.3 to 7.14.1 - refactor: Bump react-router and react-router-dom #3369 — Bump
react-routerandreact-router-dom - refactor: Bump markdown-it and graphiql #3372 — Bump
markdown-itandgraphiql(graphiql 2.x → 5.x) - refactor: Bump linkify-it and graphiql #3377 — Bump
linkify-itandgraphiql(graphiql 2.x → 5.x)
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 entrysrc/quickstart/index.js:16— quickstart entrysrc/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— centralBrowserRouter+ nestedRoutes/Route/Navigate/Outlettree (~109Routeusages, includes splat routes such ascloud_code/*)src/parse-interface-guide/routes.js—BrowserRouter/Routes/Routesrc/lib/withRouter.js— custom HOC bridging v6 hooks (useParams/useNavigate/useOutletContext/useLocation) into class components; ~60 components consume it- Other
react-router-domimporters: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/propTypeson function components: none (noX.defaultProps =assignments; the app is ~148 class components, wheredefaultPropsremains 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 (
componentWillMountin ~30 files,componentWillReceivePropsin ~33 files,componentWillUpdatein 1) — not removed in React 19, but they emit warnings and are unsafe under StrictMode/concurrent rendering. Non-blocking; recommend incremental cleanup. react-test-rendererused 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(...)withcreateRoot(container).render(...)in the three entry files listed above (the 16 → 18 step;renderis removed in 19). - Bump
react,react-dom, and devreact-test-rendererto 19; audit for the React 19 removals (ReactDOM.render/hydrate/unmountComponentAtNode, string refs, legacy Context,findDOMNode,react-dom/test-utils) — onlyReactDOM.renderapplies here. - New JSX transform is mandatory in 19 (already available via
@babel/preset-react; confirm it is enabled). refbecomes a regular prop andelement.refis deprecated;useRefnow requires an argument. Sweep for bareuseRef()calls.- Error handling changed (uncaught errors are no longer re-thrown;
onUncaughtError/onCaughtErroron 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, addreact-router@7; rewritefrom 'react-router-dom'→from 'react-router'across the ~18 files above andsrc/lib/withRouter.js. (RouterProvider/DOM-only exports move toreact-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/reactplus 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-editorworker setup). This is the largest build-config task in the migration. - CSS import path changes:
graphiql/graphiql.min.css→graphiql/style.css(updatesrc/dashboard/index.js:18). - Component API changes in
GraphQLConsole.react.js: legacy props (query/variables/headers/response/readOnly) are removed in favor ofinitialQuery/initialVariables/initialHeaders;headerEditorEnabledno longer applies. AdoptcreateGraphiQLFetcherfrom@graphiql/toolkitfor the fetcher (the existing customfetchfetcher 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:
- 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'screateRootis the real gate. - React 18 → 19 — apply the 19 removals sweep, bump
react-test-rendererto 19 (or begin the@testing-library/reactmigration), fixuseRef/ref-as-prop and error-handling changes. Rationale: keep the pure-React churn isolated from routing/GraphiQL churn. - 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 toreact-routerand removereact-router-dom. Rationale: with flags pre-adopted this becomes mechanical and won't entangle the React diff. - 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-rendererdeprecation — snapshot tests inBrowserRow,ConfigConflictDiff,ScriptResponseModalmay warn or break under React 19.- StrictMode double-invocation exposing side effects in the ~65 legacy lifecycle methods (data browser, push, analytics screens).
v7_relativeSplatPathaltering resolution of the splat routes (cloud_code/*, PIGpath="*") and any relative links beneath them.withRouterHOC — 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 -
useBeforeUnloadprompts still fire on unsaved changes (Browser filters, Playground) - Modals/popovers (createPortal) render and dismiss correctly
- Full
npm test,npm run lint, and productionnpm run buildare 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.
Activity
parse-github-assistant commentedon Jul 16, 2026
🚀 Thanks for opening this issue!
chore: remove internal planning docs from the PR (parse-community#3408)
fix: convert login entry to createRoot for React 19 (parse-community#…
test: guard login entry mounts under React 19 (parse-community#3408)
test: harden login guard e2e against port/cleanup leaks (parse-commun…
fix: open menus/popovers under React 19 by deferring the external-cli…
fix: keep popovers open when an in-menu click detaches its target (Re…
test: await server teardown in login guard e2e (parse-community#3408)
fix: read latest CLPs after class-count fetch; scope docs gitignore (p…
fix: keyboard-accessible column visibility toggle (parse-community#3408)
parseplatformorg commentedon Jul 18, 2026
🎉 This change has been released in version 9.3.0-alpha.1