Skip to content

Use React.createElement in the Jest SVG transform for React 19 - #17291

Open
TheGeekyM wants to merge 1 commit into
react:mainfrom
TheGeekyM:feat-fix-the-jest-transforms-for-re
Open

TheGeekyM wants to merge 1 commit into
react:mainfrom
TheGeekyM:feat-fix-the-jest-transforms-for-re

Conversation

@TheGeekyM

Copy link
Copy Markdown

Refs #17113

Summary

The Jest file transform turns each .svg import into a ReactComponent, and that component builds its React element by hand. React 19 does not accept that hand-built object. This PR has the component call React.createElement('svg', ...) instead. It also turns the kitchensink SVG component tests back on, since they had been skipped because of this bug.

Motivation

packages/react-scripts/config/jest/fileTransform.js is the Jest transform for every non-JS asset in a CRA project. For .svg files it creates a ReactComponent export that stands in for SVGR during tests. The component used to return a literal element object:

return {
  $$typeof: Symbol.for('react.element'),
  type: 'svg',
  ref: ref,
  key: null,
  props: Object.assign({}, props, { children: ... })
};

This depends on React's internal element shape. React 19 changed that shape in two ways:

  • Elements are now tagged with Symbol.for('react.transitional.element'). React 19 no longer accepts an object tagged react.element as an element, so rendering the component fails.
  • ref is now a regular prop (element.props.ref). The top-level element.ref field is deprecated.

Any project that runs react-scripts test on React 19 and renders a component imported with import { ReactComponent } from './x.svg' hits this. The repo already shows the problem: the kitchensink fixture SvgComponent.test.js was marked describe.skip with the comment // TODO: these fail with React 19 due to the JSX transform mismatch. Both its render test and its ref-forwarding test were disabled.

What changed

  • packages/react-scripts/config/jest/fileTransform.js: The forwardRef render function in the generated SVG module now returns React.createElement('svg', Object.assign({}, props, { ref: ref }), filename). React builds the element itself, so it always has the right $$typeof and ref handling for whichever React version is installed. Non-SVG files are unchanged and still export only the basename.
  • packages/react-scripts/fixtures/kitchensink/template/src/features/webpack/SvgComponent.test.js: Changed describe.skip back to describe and removed the TODO comment. The existing render and ref tests now cover the fix end to end.
  • packages/react-scripts/config/jest/__tests__/fileTransform.test.js (new): Unit tests for the transform's output that don't require building the kitchensink app.

How it works

The generated module still requires react from the project being tested. The only difference is that the element now comes from React.createElement instead of an object literal:

  • ref goes through the config object. React 17/18 move it into element.ref. React 19 keeps it in element.props.ref. In both cases React DOM attaches it to the rendered <svg> node.
  • The filename is passed as the third argument, so it becomes props.children as before.
  • Object.assign({}, props, ...) copies the props, so the caller's props object is never modified. The old code did the same.
  • The component is still a forwardRef named Svg<PascalCaseFilename>, which matches SVGR's naming. Snapshots and component names in test output stay the same.

Alternatives I considered:

  • Checking React.version and choosing react.element or react.transitional.element: This keeps the code tied to React internals. It would also need to copy other details of the element shape (_owner, _store, how ref is stored) that change between versions. It would break again the next time the shape changes.
  • Using react/jsx-runtime's jsx(): This works, but createElement is the stable public API that every supported React version has. It is enough for one element with static children.
  • Mapping .svg to a static mock file through moduleNameMapper: That would change the transform's public behavior (per-file component names, the default export) for every project. It's a much bigger change than this bug needs.

This PR does not touch the other Jest transforms and does not add a React 17/18/19 CI matrix. Those were part of a larger proposal and are better handled separately.

Tests

New file: packages/react-scripts/config/jest/__tests__/fileTransform.test.js. It runs the transform's output through new Function and checks the module it exports:

  • Non-SVG files:
    • exports the file's basename (logo.png).
    • the generated code is exactly module.exports = "data.txt"; and does not reference React.
  • SVG files:
    • default is the basename and __esModule is true.
    • ReactComponent is a forwardRef whose render function is named SvgMyLogo, the same as SVGR.
    • the render output passes React.isValidElement for the installed React version. This is the regression check for React 19.
    • the element type is svg and its children are the filename.
    • props such as className and width are passed to the <svg>.
    • the ref is forwarded. A small getRef helper reads props.ref (React 19) or element.ref (React 17/18), so the test works on all three.
    • the props object passed in is not modified.
    • ReactDOMServer.renderToStaticMarkup outputs <svg class="icon">my-logo.svg</svg>.

Re-enabled: packages/react-scripts/fixtures/kitchensink/template/src/features/webpack/SvgComponent.test.js

  • renders without crashing: renders SvgComponent with createRoot and checks that the text content is logo.svg.
  • svg root element equals the passed ref: renders SvgComponentWithRef and checks that the ref points to the single rendered <svg> DOM node.

To run:

npx jest 'packages/react-scripts/config/jest/__tests__/fileTransform.test.js' 'packages/react-scripts/fixtures/kitchensink/template/src/features/webpack/SvgComponent.test.js'

The kitchensink test also runs in the existing kitchensink e2e flow, which uses the transform through react-scripts test.

Compatibility & risk

  • Backwards compatibility: React.createElement with a ref in the config has worked the same way since long before React 17. The rendered DOM, the component name, the default export and the non-SVG path are all unchanged. On React 17/18 the element now also has the internal fields that createElement adds, so it is closer to a real element than the hand-built one was.
  • Snapshots: Projects that snapshot the rendered DOM or a react-test-renderer tree should see no change. I would only expect a difference for a snapshot of the raw element object returned by ReactComponent.render(...), which is unusual.
  • Performance: No measurable impact. It is one createElement call per render, which is what SVGR's real output does anyway.
  • Migration: None needed. Users get the fix when they upgrade react-scripts.
  • Worth a closer look: how ref: null is handled when no ref is passed (React 17/18 set element.ref = null, React 19 puts ref: null in props; both render the same), and whether the new config/jest/__tests__ file is picked up by the repo's root Jest config in CI.

@meta-cla meta-cla Bot added the CLA Signed label Nov 6, 2026
@TheGeekyM
TheGeekyM marked this pull request as ready for review November 6, 2026 07:18

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant