build: upgrade frontend React 18 to 19 - #1708
Draft
shivoomiess wants to merge 5 commits into
Draft
Conversation
shivoomiess
force-pushed
the
build/frontend-react-19
branch
from
August 6, 2026 09:20
5c0c9cf to
5693546
Compare
shivoomiess
force-pushed
the
build/frontend-react-19
branch
from
August 6, 2026 11:41
5693546 to
3c9e675
Compare
React 19.2.8, with `@types/react` 19.2.18 and `@types/react-dom` 19.2.4.
Prerequisite for @material-table/core v8, which peers `react >=19.0.0`.
Three dependencies capped at React 18 and had to move with it:
- `@hello-pangea/dnd` 16.6.0 -> 18.0.1 (peer was `^16.8.5 || ^17 || ^18`).
material-table v8 wants ^18.0.1 too, so this covers both.
- `@tinymce/tinymce-react` 5.1.1 -> 6.3.0. v6 peers `tinymce ^8 || ^7 || ^6 ||
^5.5.1`, and the repo is on ^7.5.1, so tinymce itself is unaffected.
- `html-react-parser` 1.4.12 -> 6.1.5. All six call sites use the bare
`parse(html)` form with no `replace` options, so nothing else changed.
Everything else was already permissive. reactflow 11 peers `>=17` and installs,
but it is superseded by @xyflow/react v12 - left alone deliberately, it is the
whole workflow editor and deserves its own change.
Adds an `overrides` block pinning `@types/react` and `@types/react-dom` to 19.
@material-table/core dragged in a nested `@types/react@18.3.31`, and the two
copies disagreed about `ReactNode` and about ref types, which alone accounted for
19 of the type errors - all 17 in materialIcons.tsx among them.
That nested copy was also the only thing still declaring the **global** `JSX`
namespace: @types/react 19 moved it to `React.JSX`, and react-i18next's
`declare global` only augments `IntrinsicAttributes`. So every `JSX.Element`
annotation was compiling by accident and would have broken at once when
material-table v8 lands.
Rather than import `JSX` back into each file, the 54 `): JSX.Element` return
annotations are simply deleted - TypeScript infers component return types, and
the annotation never carried information. The remaining 19 uses become the type
that was actually meant:
- `ReactNode` for anything renderable (`icon`, `extra`, `content`, `info`,
`actionIcon`, the `answerRenderer` return, `alertText[]`).
- `ReactElement | null` for the two questionary form factories, matching the
sibling `questionaryComponent` declaration in the same interface.
- `React.Attributes` for the one `JSX.IntrinsicAttributes` use.
- `React.MouseEvent<HTMLElement>` for four `React.MouseEvent<JSX.Element>`,
which were always wrong: the parameter is a DOM element, not a React element.
It only compiled because the parameter is unconstrained.
- `<SomeContext.Provider>` -> `<SomeContext>` in all 17 places. React 19 renders
a context directly and deprecates the `.Provider` child.
- `forwardRef` removed from the three modal `Transition` components; React 19
passes `ref` as an ordinary prop. The 18 wrappers in `materialIcons.tsx` have
to stay: @material-table/core 6.4.4 types every icon as
`ForwardRefExoticComponent<any> & RefAttributes<SVGSVGElement>`, and an
ordinary function component does not satisfy `ExoticComponent`'s `$$typeof`.
They come out with material-table v8.
- `useReducerWithMiddleWares` is parameterised by state and action instead of by
a reducer type. It previously took `R extends Reducer<any, any>` and pulled the
parts back out with `ReducerState<R>` and `ReducerAction<R>`; React 19 removed
`ReducerAction`, which is exactly the indirection it was removed for. The three
call sites now read `<Workflow, Event>` rather than `<Reducer<Workflow, Event>>`.
- Removes 18 dead `propTypes` blocks across 17 files, and the `prop-types` import
from each. React 19 ignores `propTypes` entirely, and every one of these
components is already typed in TypeScript. `prop-types` was never a declared
dependency - it only resolved transitively.
- `FunctionComponent`'s return type widened to `ReactNode | Promise<ReactNode>`
for async components. `QuestionRenderer` and `AnswerRenderer` were typed that
way, but all four call sites invoke them directly and embed the result; none
renders them as JSX. Retyped as plain functions returning `ReactNode`.
- `ReactElement`'s props default went `any` -> `unknown`, which broke
`step.props` in `MultistepWizard` and `cloneElement(children, {isInsideModal})`
in both review modals.
- `useRef<T>(null)` now yields `RefObject<T | null>`, so `WorkflowCanvas` has to
declare its `reactFlowWrapper` prop that way. Five argless `useRef()` calls
gained an explicit `undefined`.
- `Dispatch` became contravariant in its parameter, exposing a real latent bug:
`handleRole` was declared `Dispatch<string | null | undefined>` but the
SELECTROLE reducer calls `payload.toUpperCase()`. Narrowed to
`(role: string) => void`, which is what the sole caller already passes.
Checked and clean: no findDOMNode, ReactDOM.render/hydrate, react-dom/test-utils,
unmountComponentAtNode or createFactory. `index.tsx` already uses `createRoot`.
The four `defaultProps` hits are MUI theme component defaults, not React's
removed `Component.defaultProps`.
`tsc --noEmit`, `eslint` and `vite build` all pass. There is no unit test runner
in the frontend, so none of this is exercised at runtime by CI before Cypress.
React 19 did not remove `React.FC`, so this is not forced by the upgrade - but it
is the last of the pre-19 idioms left in the tree, and React's own documentation
has not used it for years. Annotating the parameter says the same thing without
routing the props through a wrapper type.
-const StatusPicker: React.FC<StatusPickerProps> = ({ statuses }) => {
+const StatusPicker = ({ statuses }: StatusPickerProps) => {
14 declarations across 14 files. No behaviour change: `React.FC` stopped implying
`children` in the React 18 types, so nothing depended on it here.
Left alone: the three `TableProps<FunctionComponent<unknown>>` type arguments in
the questionary review components. `FunctionComponent` is a legitimate
`ElementType` there and is not deprecated - it is a type argument, not a
component annotation.
… warning
`npm run lint` passes `--quiet`, so warnings never surfaced. There were 41.
Two causes accounted for half of them, both fixable at the source.
**`vite.config.mts` was outside the project config.** The custom block matched
`**/*.{ts,tsx,js,jsx}`, which `.mts` does not. So that one file picked up
`react.configs.flat.recommended` - which is applied globally with no `files` key
- but not the sibling `settings.react.version`, and eslint-plugin-react printed
"React version not specified in eslint-plugin-react settings" on every single
lint run. Widened the glob to cover mts/cts/mjs/cjs.
**Three components exported the same symbol twice**, as both a named and a
default export, which is what `import/no-named-as-default` was reporting at all
20 of its call sites: `SuperMaterialTable` (13), `FormikUICustomTable` (6) and
`DenseMaterialTable` (1). Nothing imports the named form anywhere, so dropping
the `export` keyword from the declaration fixes every call site without touching
a single import.
41 warnings -> 21. The rest are left deliberately:
- 18 `react-hooks/exhaustive-deps`. All pre-existing - none of them sits in a
dependency array this branch touched - and each is a behaviour change that
wants e2e verification rather than a bulk edit.
- 2 `import/no-named-as-default-member` for `tseslint.configs` and `i18n.use`.
Both are the documented way to use those packages.
- 1 `import/no-named-as-default` for `ReactFlow`, which is reactflow's own
double export and not ours to fix.
…ation
Three pages - Shipment, Visit and Feedback templates - crashed with
"Maximum call stack size exceeded" inside deepmerge as soon as their table was
empty.
@material-table/core deep-merges the `localization` prop against its defaults
(`store/LocalizationStore.js`). deepmerge skips React elements via
var REACT_ELEMENT_TYPE = Symbol.for('react.element');
function isReactElement(value) { return value.$$typeof === REACT_ELEMENT_TYPE }
React 19 renamed that symbol to `react.transitional.element`, so the guard never
matches. deepmerge then treats the element as a plain object and recurses through
its internals until the stack overflows.
The three pages pass JSX as `emptyDataSourceMessage`, which `TemplatesTable` put
straight into `localization.body`. material-table's own types declare that field
as `React.ReactNode`, so this was documented usage, not misuse - the library is
merging a field it invites you to fill with elements.
`localization` now only ever receives plain strings. A richer message is rendered
under the table instead, so the wording and formatting are unchanged.
**This is not fixed by upgrading material-table.** v8.0.3 depends on the same
`deepmerge ^4.3.1` and its `LocalizationStore` still calls
`merge(defaults, props.localization ?? {})` with no `isMergeableObject` option.
deepmerge's latest release is 4.3.1 and predates React 19, so there is no version
to upgrade to either.
A patched deepmerge was considered and rejected: formik pulls its own
`deepmerge@2.2.1` with the identical stale guard, so an override would either
miss it or change formik's merge behaviour.
Only `localization` is deep-merged - `icons`, `options` and `components` are not
- so no other prop is affected. `tableLocalization` in utils is strings only.
Logging in looped between / and /external-auth, with the backend reporting `invalid_grant (grant request is invalid)` from the OpenID client. An authorization code is single-use, and the code was reaching the token endpoint twice. ExternalAuth guarded against this with a `useRef`, which holds for StrictMode's simulated remount but resets whenever the component genuinely remounts within a page load. The second mount then replayed a code the provider had already spent. The exchange is now memoised at module scope, keyed on the code, so a remount joins the in-flight request instead of starting a second one. Joining rather than skipping matters: a later mount that merely skipped would sit on "Please wait" forever whenever the first exchange failed. What remounts the component is still unidentified. The provider gates in SettingsContextProvider and FeatureContextProvider both resolve before ExternalAuth mounts, IdleContextPicker cannot flip with STFC_IDLE_TIMER off, and plain StrictMode is what the existing ref already covers. The fix does not depend on the answer, since exchanging a single-use code twice is a client bug whatever triggers the second attempt. The same code is on develop, so the bug is not new here. It is placed on this branch because React 19 is the change most likely to have altered the mount behaviour that triggers it.
shivoomiess
force-pushed
the
build/frontend-react-19
branch
from
August 10, 2026 12:22
3c9e675 to
5e242f7
Compare
yoganandaness
marked this pull request as ready for review
August 21, 2026 08:29
yoganandaness
requested review from
Bhaswati1148
and removed request for
a team
August 21, 2026 08:29
yoganandaness
approved these changes
Aug 21, 2026
This was referenced Aug 25, 2026
shivoomiess
marked this pull request as draft
August 25, 2026 20:46
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.
Description
This PR upgrades the frontend React version from 18 to 19 and updates the associated dependencies accordingly.
Motivation and Context
Upgrading React to version 19 is crucial for improving the performance and stability of the application, as it comes with several bug fixes and new features. This upgrade also ensures compatibility with the latest libraries and tools in the React ecosystem, making future developments easier and more efficient.
Changes
apps/frontend/eslint.config.jsto include additional types.apps/frontend/package-lock.jsonto their respective latest versions, including "@hello-pangea/dnd", "@tinymce/tinymce-react", "html-react-parser", "react", and "react-dom".apps/frontend/package-lock.jsonto match the updated React version.apps/frontend/package-lock.jsonto their respective registries.How Has This Been Tested?
Fixes Jira Issue
https://jira.ess.eu//browse/
Depends On
Tests included/Docs Updated?