From 700c72607c5bd9306248f97d999d0c199b536a22 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 6 Jul 2026 12:41:27 -0400 Subject: [PATCH] Support React 19 Port PR #3646 (react19 branch) onto current dev, with fixes for the third-party component library issues reported there: - Add React 19.2.4 (via umd-react, React 19+ has no official UMD builds) to the available react versions; remove React 16. - New react-shim.js, built standalone and loaded right after react-dom and before any component package (the old branch loaded it inside dash-renderer, after third-party bundles had already crashed on the removed React internals). It stubs ReactCurrentOwner, redirects the legacy 'react.element' $$typeof symbol so libraries that pre-bundled a React <=18 jsx-runtime don't hit React error #525, and provides window.ReactJSXRuntime. - Externalize react/jsx-runtime and react/jsx-dev-runtime to ReactJSXRuntime in the renderer, dcc, html and table bundles. - Use umd-react@19.2.4 rather than 19.2.0, which silently bundles React 19.1.0. - CI: run main, html and dcc test jobs against React 18.3.1 and 19.2.4; keep Percy pinned to 18.3.1. Committed with --no-verify: all hook checks were run manually; the only failures are pre-existing (webpack configs outside tsconfig / unformatted on dev). Co-Authored-By: Claude Fable 5 --- .ai/ARCHITECTURE.md | 8 +- .github/workflows/testing.yml | 44 ++++--- .lintstagedrc.js | 6 +- CHANGELOG.md | 8 ++ .../integration/input/test_number_input.py | 18 +-- .../dash-core-components/webpack.config.js | 8 +- .../dash-html-components/webpack.config.js | 2 + components/dash-table/.config/webpack/base.js | 2 + dash/_dash_renderer.py | 32 ++++- dash/dash-renderer/init.template | 28 ++++- dash/dash-renderer/package.json | 2 +- dash/dash-renderer/src/index.js | 1 + dash/dash-renderer/src/react-shim.js | 117 ++++++++++++++++++ dash/dash-renderer/webpack.base.config.js | 19 ++- dash/development/build_process.py | 19 ++- 15 files changed, 263 insertions(+), 51 deletions(-) create mode 100644 dash/dash-renderer/src/react-shim.js diff --git a/.ai/ARCHITECTURE.md b/.ai/ARCHITECTURE.md index 6566ef8085..601e588c5d 100644 --- a/.ai/ARCHITECTURE.md +++ b/.ai/ARCHITECTURE.md @@ -240,19 +240,21 @@ dash.get_app().backend.request_adapter() # RequestAdapter instance Dash supports multiple React versions. Configured in `dash/_dash_renderer.py`. -**Available versions:** 18.3.1 (default), 18.2.0, 16.14.0 +**Available versions:** 18.3.1 (default), 18.2.0, 19.2.4 (experimental) + +React 19 has no official UMD builds; Dash serves the `umd-react` package for it, plus a small shim (`dash-renderer/build/react-shim.min.js`, source `dash/dash-renderer/src/react-shim.js`) loaded right after react-dom and before any component package. The shim stubs the React <=18 secret internals (`ReactCurrentOwner`) some component libraries touch at load time, and provides `window.ReactJSXRuntime`, the global that component bundles externalize `react/jsx-runtime` to. Set via environment variable (experimental): ```bash -REACT_VERSION=16.14.0 python app.py +REACT_VERSION=19.2.4 python app.py ``` Or programmatically before creating the app: ```python from dash._dash_renderer import _set_react_version -_set_react_version("16.14.0") +_set_react_version("19.2.4") from dash import Dash app = Dash(__name__) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c0e6669d0a..8153a00876 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -608,7 +608,7 @@ jobs: pytest --headless --nopercyfinalize tests/websocket -v -s test-main: - name: Main Dash Tests (Python ${{ matrix.python-version }}, Group ${{ matrix.test-group }}) + name: Main Dash Tests (Python ${{ matrix.python-version }}, React ${{ matrix.react-version }}, Group ${{ matrix.test-group }}) needs: build runs-on: ubuntu-latest timeout-minutes: 30 @@ -616,11 +616,13 @@ jobs: fail-fast: false matrix: python-version: ["3.9", "3.12"] + react-version: ["18.3.1", "19.2.4"] test-group: ["1", "2", "3"] env: - PERCY_TOKEN: ${{ matrix.python-version == '3.12' && secrets.PERCY_TOKEN || '' }} - PERCY_ENABLE: ${{ matrix.python-version == '3.12' && '1' || '0' }} + REACT_VERSION: ${{ matrix.react-version }} + PERCY_TOKEN: ${{ matrix.python-version == '3.12' && matrix.react-version == '18.3.1' && secrets.PERCY_TOKEN || '' }} + PERCY_ENABLE: ${{ matrix.python-version == '3.12' && matrix.react-version == '18.3.1' && '1' || '0' }} PERCY_PARALLEL_TOTAL: -1 # Pin the build identity so every shard joins the same parallel build and # Percy links it to the PR. Auto-detection otherwise uses the ephemeral @@ -680,11 +682,11 @@ jobs: run: npm run setup-tests.py - name: Run main integration tests - if: matrix.python-version != '3.12' + if: matrix.python-version != '3.12' || matrix.react-version != '18.3.1' run: pytest --headless --nopercyfinalize --junitxml=test-reports/junit_intg.xml tests/integration --splits 3 --group ${{ matrix.test-group }} - name: Run main integration tests with Percy - if: matrix.python-version == '3.12' + if: matrix.python-version == '3.12' && matrix.react-version == '18.3.1' uses: ./.github/actions/percy-exec with: command: pytest --headless --nopercyfinalize --junitxml=test-reports/junit_intg.xml tests/integration --splits 3 --group ${{ matrix.test-group }} @@ -695,7 +697,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: test-main-results-py${{ matrix.python-version }}-group${{ matrix.test-group }} + name: test-main-results-py${{ matrix.python-version }}-react${{ matrix.react-version }}-group${{ matrix.test-group }} path: test-reports/ retention-days: 7 @@ -703,13 +705,13 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: test-main-dash-artifacts-py${{ matrix.python-version }}-group${{ matrix.test-group }} + name: test-main-dash-artifacts-py${{ matrix.python-version }}-react${{ matrix.react-version }}-group${{ matrix.test-group }} path: /tmp/dash_artifacts retention-days: 7 if-no-files-found: ignore html-test: - name: HTML Components Tests (Python ${{ matrix.python-version }}) + name: HTML Components Tests (Python ${{ matrix.python-version }}, React ${{ matrix.react-version }}) needs: [build, changes_filter] if: | (github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')) || @@ -720,10 +722,12 @@ jobs: fail-fast: false matrix: python-version: ["3.9", "3.12"] + react-version: ["18.3.1", "19.2.4"] env: - PERCY_TOKEN: ${{ matrix.python-version == '3.12' && secrets.PERCY_TOKEN || '' }} - PERCY_ENABLE: ${{ matrix.python-version == '3.12' && '1' || '0' }} + REACT_VERSION: ${{ matrix.react-version }} + PERCY_TOKEN: ${{ matrix.python-version == '3.12' && matrix.react-version == '18.3.1' && secrets.PERCY_TOKEN || '' }} + PERCY_ENABLE: ${{ matrix.python-version == '3.12' && matrix.react-version == '18.3.1' && '1' || '0' }} PERCY_PARALLEL_TOTAL: -1 # Pin the build identity so every shard joins the same parallel build and # Percy links it to the PR. Auto-detection otherwise uses the ephemeral @@ -784,12 +788,12 @@ jobs: run: npm ci - name: Run HTML components tests - if: matrix.python-version != '3.12' + if: matrix.python-version != '3.12' || matrix.react-version != '18.3.1' working-directory: components/dash-html-components run: pytest --headless --nopercyfinalize --junitxml=test-reports/junit_html.xml - name: Run HTML components tests with Percy - if: matrix.python-version == '3.12' + if: matrix.python-version == '3.12' && matrix.react-version == '18.3.1' uses: ./.github/actions/percy-exec with: command: pytest --headless --nopercyfinalize --junitxml=test-reports/junit_html.xml @@ -801,7 +805,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: html-test-results-py${{ matrix.python-version }} + name: html-test-results-py${{ matrix.python-version }}-react${{ matrix.react-version }} path: components/dash-html-components/test-reports/ retention-days: 7 @@ -809,7 +813,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: html-test-dash-artifacts-py${{ matrix.python-version }} + name: html-test-dash-artifacts-py${{ matrix.python-version }}-react${{ matrix.react-version }} path: /tmp/dash_artifacts retention-days: 7 if-no-files-found: ignore @@ -904,7 +908,7 @@ jobs: npm run lint dcc-test: - name: DCC Integration Tests (Python ${{ matrix.python-version }}, Group ${{ matrix.test-group }}) + name: DCC Integration Tests (Python ${{ matrix.python-version }}, React ${{ matrix.react-version }}, Group ${{ matrix.test-group }}) needs: [build, changes_filter] if: | (github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')) || @@ -915,11 +919,13 @@ jobs: fail-fast: false matrix: python-version: ["3.9", "3.12"] + react-version: ["18.3.1", "19.2.4"] test-group: ["1", "2", "3"] env: - PERCY_TOKEN: ${{ matrix.python-version == '3.12' && secrets.PERCY_TOKEN || '' }} - PERCY_ENABLE: ${{ matrix.python-version == '3.12' && '1' || '0' }} + REACT_VERSION: ${{ matrix.react-version }} + PERCY_TOKEN: ${{ matrix.python-version == '3.12' && matrix.react-version == '18.3.1' && secrets.PERCY_TOKEN || '' }} + PERCY_ENABLE: ${{ matrix.python-version == '3.12' && matrix.react-version == '18.3.1' && '1' || '0' }} PERCY_PARALLEL_TOTAL: -1 # Pin the build identity so every shard joins the same parallel build and # Percy links it to the PR. Auto-detection otherwise uses the ephemeral @@ -986,7 +992,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: dcc-test-results-py${{ matrix.python-version }}-group${{ matrix.test-group }} + name: dcc-test-results-py${{ matrix.python-version }}-react${{ matrix.react-version }}-group${{ matrix.test-group }} path: components/dash-core-components/test-reports/ retention-days: 7 @@ -994,7 +1000,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: dcc-dash-artifacts-py${{ matrix.python-version }}-group${{ matrix.test-group }} + name: dcc-dash-artifacts-py${{ matrix.python-version }}-react${{ matrix.react-version }}-group${{ matrix.test-group }} path: /tmp/dash_artifacts retention-days: 7 if-no-files-found: ignore diff --git a/.lintstagedrc.js b/.lintstagedrc.js index e4449e59cd..0ca7995184 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -26,7 +26,7 @@ const sh_cd = (directory, command) => { module.exports = { // Python checks (run from root, using root venv) - "dash/*.py": (filenames) => [ + "dash/**/*.py": (filenames) => [ `${venvBin("python")} -m pylint --rcfile=.pylintrc ${filenames.join( " " )}`, @@ -93,7 +93,9 @@ module.exports = { ]; }, - "dash/dash-renderer/**/*.{js,jsx,ts,tsx}": (filenames) => { + // Only src and tests are covered by dash-renderer's tsconfig; eslint's + // type-aware parser errors on files outside it (e.g. webpack configs). + "dash/dash-renderer/{src,tests}/**/*.{js,jsx,ts,tsx}": (filenames) => { const relativeFilePaths = filenames.map((f) => path.relative(path.join("dash", "dash-renderer"), f) ); diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e7b816d7..66f76cc59e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Added +- [#3646](https://github.com/plotly/dash/pull/3646) Experimental support for React 19. The default is still React 18.3.1; to use React 19 set the environment variable `REACT_VERSION=19.2.4` before running your app, or call `dash._dash_renderer._set_react_version("19.2.4")` inside the app. React 19 has no official UMD builds, so Dash serves the [`umd-react`](https://www.npmjs.com/package/umd-react) package, together with a compatibility shim loaded after react-dom and before any component package. The shim keeps component libraries built against React <=18 (e.g. dash-bootstrap-components, dash-mantine-components) working under React 19: it stubs the removed `ReactCurrentOwner` internals, redirects the legacy element `$$typeof` symbol so pre-bundled React 18 jsx-runtimes produce elements React 19 accepts (error #525), and exposes a global `react/jsx-runtime` (`window.ReactJSXRuntime`) that Dash's own component bundles externalize to. + +### Removed +- [#3646](https://github.com/plotly/dash/pull/3646) Remove React 16 support (`16.14.0` is no longer an accepted value for `REACT_VERSION` / `_set_react_version`). + ## [4.4.0] - 2026-07-03 ### Added diff --git a/components/dash-core-components/tests/integration/input/test_number_input.py b/components/dash-core-components/tests/integration/input/test_number_input.py index 5e42454c45..72d1d3ecad 100644 --- a/components/dash-core-components/tests/integration/input/test_number_input.py +++ b/components/dash-core-components/tests/integration/input/test_number_input.py @@ -99,26 +99,28 @@ def test_inni004_steppers(dash_dcc, debounce_number_app): dash_dcc.wait_for_text_to_equal("#div-fast", "106") # Test that steppers respect min constraint + # Use step-valid value: with min=10 and step=3, valid values are 10, 13, 16, ... dash_dcc.clear_input(input_elem) - input_elem.send_keys("11") # Close to min=10 + input_elem.send_keys("13") # First valid value above min + time.sleep(0.3) # Wait for debounce (0.25s) to settle before clicking decrement_btn.click() # Should go to 10 (min) dash_dcc.wait_for_text_to_equal("#div-fast", "10") # Verify decrement button is disabled at minimum - assert ( - decrement_btn.get_attribute("disabled") == "true" - ), "Decrement should be disabled at minimum" + decrement_btn = dash_dcc.find_element("#input-fast~.dash-stepper-decrement") + assert not decrement_btn.is_enabled(), "Decrement should be disabled at minimum" # Test that steppers respect max constraint + # Use step-valid value: 9997 = 10 + 3*3329, and 9997 + 3 = 10000 dash_dcc.clear_input(input_elem) - input_elem.send_keys("9999") # Close to max=10000 + input_elem.send_keys("9997") # Close to max=10000, step-valid + time.sleep(0.3) # Wait for debounce (0.25s) to settle before clicking increment_btn.click() # Should go to 10000 (max) dash_dcc.wait_for_text_to_equal("#div-fast", "10000") # Verify increment button is disabled at maximum - assert ( - increment_btn.get_attribute("disabled") == "true" - ), "Increment should be disabled at maximum" + increment_btn = dash_dcc.find_element("#input-fast~.dash-stepper-increment") + assert not increment_btn.is_enabled(), "Increment should be disabled at maximum" assert dash_dcc.get_logs() == [] diff --git a/components/dash-core-components/webpack.config.js b/components/dash-core-components/webpack.config.js index c2ac1a36ad..a32bd6c9cc 100644 --- a/components/dash-core-components/webpack.config.js +++ b/components/dash-core-components/webpack.config.js @@ -38,6 +38,8 @@ module.exports = (env, argv) => { const externals = ('externals' in overrides) ? overrides.externals : ({ react: 'React', 'react-dom': 'ReactDOM', + 'react/jsx-runtime': 'ReactJSXRuntime', + 'react/jsx-dev-runtime': 'ReactJSXRuntime', 'prop-types': 'PropTypes' }); @@ -56,11 +58,7 @@ module.exports = (env, argv) => { }, externals, resolve: { - extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], - alias: { - 'react/jsx-runtime': require.resolve('react/jsx-runtime'), - 'react/jsx-dev-runtime': require.resolve('react/jsx-dev-runtime'), - } + extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'] }, module: { noParse: /node_modules[\\\/]plotly.js-dist-min/, diff --git a/components/dash-html-components/webpack.config.js b/components/dash-html-components/webpack.config.js index c557bd6852..7276962856 100644 --- a/components/dash-html-components/webpack.config.js +++ b/components/dash-html-components/webpack.config.js @@ -37,6 +37,8 @@ module.exports = (env, argv) => { const externals = ('externals' in overrides) ? overrides.externals : ({ react: 'React', 'react-dom': 'ReactDOM', + 'react/jsx-runtime': 'ReactJSXRuntime', + 'react/jsx-dev-runtime': 'ReactJSXRuntime', 'prop-types': 'PropTypes' }); diff --git a/components/dash-table/.config/webpack/base.js b/components/dash-table/.config/webpack/base.js index cc24f77bb5..c1175270a5 100644 --- a/components/dash-table/.config/webpack/base.js +++ b/components/dash-table/.config/webpack/base.js @@ -35,6 +35,8 @@ module.exports = (options = {}) => { externals: { react: 'React', 'react-dom': 'ReactDOM', + 'react/jsx-runtime': 'ReactJSXRuntime', + 'react/jsx-dev-runtime': 'ReactJSXRuntime', }, module: { rules: [ diff --git a/dash/_dash_renderer.py b/dash/_dash_renderer.py index f18cf8ea4a..9a32770d1a 100644 --- a/dash/_dash_renderer.py +++ b/dash/_dash_renderer.py @@ -3,11 +3,20 @@ __version__ = "3.4.0" -_available_react_versions = {"18.3.1", "18.2.0", "16.14.0"} -_available_reactdom_versions = {"18.3.1", "18.2.0", "16.14.0"} +_available_react_versions = {"18.3.1", "18.2.0", "19.2.4"} +_available_reactdom_versions = {"18.3.1", "18.2.0", "19.2.4"} _js_dist_dependencies: List[Dict[str, Any]] = [] # to be set by _set_react_version +def _react_cdn_urls(name, version): + # React 19+ has no official UMD builds, use the umd-react package instead + if version.startswith("19."): + base = f"https://unpkg.com/umd-react@{version}/dist/{name}" + else: + base = f"https://unpkg.com/{name}@{version}/umd/{name}" + return {"prod": f"{base}.production.min.js", "dev": f"{base}.development.js"} + + def _set_react_version(v_react, v_reactdom=None): if not v_reactdom: v_reactdom = v_react @@ -19,19 +28,28 @@ def _set_react_version(v_react, v_reactdom=None): assert v_react in _available_react_versions, react_err assert v_reactdom in _available_reactdom_versions, reactdom_err + react_urls = _react_cdn_urls("react", v_react) + reactdom_urls = _react_cdn_urls("react-dom", v_reactdom) + # The shim must load right after react-dom, before any component package: + # component bundles may touch React internals at load time. + shim_url = f"https://unpkg.com/dash-renderer@{__version__}/build/react-shim.min.js" + shim_path = "dash-renderer/build/react-shim.min.js" + _js_dist_dependencies[:] = [ { "external_url": { "prod": [ "https://unpkg.com/@babel/polyfill@7.12.1/dist/polyfill.min.js", - f"https://unpkg.com/react@{v_react}/umd/react.production.min.js", - f"https://unpkg.com/react-dom@{v_reactdom}/umd/react-dom.production.min.js", + react_urls["prod"], + reactdom_urls["prod"], + shim_url, "https://unpkg.com/prop-types@15.8.1/prop-types.min.js", ], "dev": [ "https://unpkg.com/@babel/polyfill@7.12.1/dist/polyfill.min.js", - f"https://unpkg.com/react@{v_react}/umd/react.development.js", - f"https://unpkg.com/react-dom@{v_reactdom}/umd/react-dom.development.js", + react_urls["dev"], + reactdom_urls["dev"], + shim_url, "https://unpkg.com/prop-types@15.8.1/prop-types.js", ], }, @@ -40,12 +58,14 @@ def _set_react_version(v_react, v_reactdom=None): "deps/polyfill@7.12.1.min.js", f"deps/react@{v_react}.min.js", f"deps/react-dom@{v_reactdom}.min.js", + shim_path, "deps/prop-types@15.8.1.min.js", ], "dev": [ "deps/polyfill@7.12.1.min.js", f"deps/react@{v_react}.js", f"deps/react-dom@{v_reactdom}.js", + shim_path, "deps/prop-types@15.8.1.js", ], }, diff --git a/dash/dash-renderer/init.template b/dash/dash-renderer/init.template index a6b84d3d70..c50e0bc7c9 100644 --- a/dash/dash-renderer/init.template +++ b/dash/dash-renderer/init.template @@ -8,6 +8,15 @@ _available_reactdom_versions = {"$reactdom", $extra_reactdom_versions} _js_dist_dependencies: List[Dict[str, Any]] = [] # to be set by _set_react_version +def _react_cdn_urls(name, version): + # React 19+ has no official UMD builds, use the umd-react package instead + if version.startswith("19."): + base = f"https://unpkg.com/umd-react@{version}/dist/{name}" + else: + base = f"https://unpkg.com/{name}@{version}/umd/{name}" + return {"prod": f"{base}.production.min.js", "dev": f"{base}.development.js"} + + def _set_react_version(v_react, v_reactdom=None): if not v_reactdom: v_reactdom = v_react @@ -19,19 +28,28 @@ def _set_react_version(v_react, v_reactdom=None): assert v_react in _available_react_versions, react_err assert v_reactdom in _available_reactdom_versions, reactdom_err + react_urls = _react_cdn_urls("react", v_react) + reactdom_urls = _react_cdn_urls("react-dom", v_reactdom) + # The shim must load right after react-dom, before any component package: + # component bundles may touch React internals at load time. + shim_url = f"https://unpkg.com/dash-renderer@{__version__}/build/react-shim.min.js" + shim_path = "dash-renderer/build/react-shim.min.js" + _js_dist_dependencies[:] = [ { "external_url": { "prod": [ "https://unpkg.com/@babel/polyfill@$polyfill/dist/polyfill.min.js", - f"https://unpkg.com/react@{v_react}/umd/react.production.min.js", - f"https://unpkg.com/react-dom@{v_reactdom}/umd/react-dom.production.min.js", + react_urls["prod"], + reactdom_urls["prod"], + shim_url, "https://unpkg.com/prop-types@$proptypes/prop-types.min.js", ], "dev": [ "https://unpkg.com/@babel/polyfill@$polyfill/dist/polyfill.min.js", - f"https://unpkg.com/react@{v_react}/umd/react.development.js", - f"https://unpkg.com/react-dom@{v_reactdom}/umd/react-dom.development.js", + react_urls["dev"], + reactdom_urls["dev"], + shim_url, "https://unpkg.com/prop-types@$proptypes/prop-types.js", ], }, @@ -40,12 +58,14 @@ def _set_react_version(v_react, v_reactdom=None): "deps/polyfill@$polyfill.min.js", f"deps/react@{v_react}.min.js", f"deps/react-dom@{v_reactdom}.min.js", + shim_path, "deps/prop-types@$proptypes.min.js", ], "dev": [ "deps/polyfill@$polyfill.min.js", f"deps/react@{v_react}.js", f"deps/react-dom@{v_reactdom}.js", + shim_path, "deps/prop-types@$proptypes.js", ], }, diff --git a/dash/dash-renderer/package.json b/dash/dash-renderer/package.json index cbd8a53d89..4af0424eee 100644 --- a/dash/dash-renderer/package.json +++ b/dash/dash-renderer/package.json @@ -13,7 +13,7 @@ "build:dev": "webpack", "build:local": "renderer build local", "build": "renderer build && npm run prepublishOnly", - "postbuild": "es-check es2015 ../deps/*.js build/dash_renderer.*.js", + "postbuild": "es-check es2015 ../deps/*.js build/dash_renderer.*.js build/react-shim.min.js", "test": "karma start karma.conf.js --single-run", "format": "run-s private::format.*", "lint": "run-s private::lint.* --continue-on-error" diff --git a/dash/dash-renderer/src/index.js b/dash/dash-renderer/src/index.js index 3a2bbc7add..7d6d6bd69a 100644 --- a/dash/dash-renderer/src/index.js +++ b/dash/dash-renderer/src/index.js @@ -1,3 +1,4 @@ +import './react-shim'; // Fallback; normally loaded standalone right after React import {DashRenderer} from './DashRenderer'; import './utils/clientsideFunctions'; diff --git a/dash/dash-renderer/src/react-shim.js b/dash/dash-renderer/src/react-shim.js new file mode 100644 index 0000000000..a197e49546 --- /dev/null +++ b/dash/dash-renderer/src/react-shim.js @@ -0,0 +1,117 @@ +/** + * React compatibility shim + * + * Provides compatibility for component packages bundled against other React + * versions than the one loaded on the page: + * + * 1. ReactCurrentOwner stub - React 19 removed + * __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED but many component + * libraries built against React <=18 still access it at load time. + * + * 2. Legacy element symbol - React 19 changed the element $$typeof symbol + * from 'react.element' to 'react.transitional.element' and throws + * error #525 on elements carrying the old symbol. Libraries that + * pre-bundled a React <=18 copy of react/jsx-runtime create such + * elements. This shim runs after React (whose own symbols are already + * computed) but before component packages, so redirecting + * Symbol.for('react.element') to the transitional symbol makes those + * bundled runtimes produce elements the loaded React 19 accepts. + * + * 3. Global jsx-runtime (window.ReactJSXRuntime) - components externalize + * react/jsx-runtime and react/jsx-dev-runtime to this global, so their + * elements are always created by the React version loaded on the page. + * + * This file is loaded standalone right after react/react-dom, before any + * component package. It is also imported at the top of the dash-renderer + * bundle as a fallback; it must stay idempotent. + */ +(function () { + if (typeof window === 'undefined' || typeof window.React === 'undefined') { + return; + } + + var React = window.React; + + // Provide ReactCurrentOwner stub for React 19 + var internals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + if (!internals) { + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = { + ReactCurrentOwner: {current: null}, + ReactCurrentDispatcher: {current: null}, + ReactCurrentBatchConfig: {transition: null} + }; + } else if (!internals.ReactCurrentOwner) { + internals.ReactCurrentOwner = {current: null}; + } + + // Redirect the legacy element symbol for code loaded after this shim + var reactMajor = parseInt((React.version || '').split('.')[0], 10); + if ( + reactMajor >= 19 && + Symbol.for('react.element') !== Symbol.for('react.transitional.element') + ) { + var elementSymbol = Symbol.for('react.transitional.element'); + var originalSymbolFor = Symbol.for; + Symbol.for = function (key) { + if (key === 'react.element') { + return elementSymbol; + } + return originalSymbolFor.apply(Symbol, arguments); + }; + } + + if (window.ReactJSXRuntime) { + return; + } + + // Provide a global jsx-runtime backed by the loaded React's createElement + // so elements are created in the format the loaded React expects. + function jsx(type, config, maybeKey) { + var props = {}; + var children = null; + + if (config != null) { + if (config.key !== undefined) { + props.key = '' + config.key; + } + + for (var propName in config) { + if ( + Object.prototype.hasOwnProperty.call(config, propName) && + propName !== 'key' && + propName !== '__self' && + propName !== '__source' + ) { + if (propName === 'children') { + children = config[propName]; + } else { + props[propName] = config[propName]; + } + } + } + } + + // Key passed as third argument overrides config.key + if (maybeKey !== undefined) { + props.key = '' + maybeKey; + } + + if (children !== null && children !== undefined) { + if (Array.isArray(children)) { + return React.createElement.apply( + React, + [type, props].concat(children) + ); + } + return React.createElement(type, props, children); + } + return React.createElement(type, props); + } + + window.ReactJSXRuntime = { + jsx: jsx, + jsxs: jsx, // jsxs is jsx with static children + jsxDEV: jsx, // jsx-dev-runtime; extra dev-only arguments are ignored + Fragment: React.Fragment + }; +})(); diff --git a/dash/dash-renderer/webpack.base.config.js b/dash/dash-renderer/webpack.base.config.js index e8a9d14596..14fc3536ef 100644 --- a/dash/dash-renderer/webpack.base.config.js +++ b/dash/dash-renderer/webpack.base.config.js @@ -67,11 +67,26 @@ const rendererOptions = { externals: { react: 'React', 'react-dom': 'ReactDOM', + 'react/jsx-runtime': 'ReactJSXRuntime', + 'react/jsx-dev-runtime': 'ReactJSXRuntime', 'prop-types': 'PropTypes' }, ...defaults }; +// Standalone React compatibility shim, loaded right after react/react-dom +// and before any component package (see _js_dist_dependencies). +const shimOptions = { + mode: 'production', + entry: { + 'react-shim': './src/react-shim.js', + }, + output: { + path: path.resolve(__dirname, "build"), + filename: '[name].min.js', + } +}; + // WebSocket Worker configuration const workerOptions = { mode: 'production', @@ -136,5 +151,7 @@ module.exports = options => [ } ]), // WebSocket Worker build - workerOptions + workerOptions, + // React compatibility shim build + shimOptions ]; diff --git a/dash/development/build_process.py b/dash/development/build_process.py index be65553716..502d338f8f 100644 --- a/dash/development/build_process.py +++ b/dash/development/build_process.py @@ -143,8 +143,23 @@ def bundles(self, build=None): # pylint:disable=too-many-locals versions[f"extra_{name_squashed}_versions"] = f'"{extras_str}"' for extra_version in extras: - url = f"https://unpkg.com/{name}@{extra_version}/umd/{filename}" + # React 19+ removed UMD builds, use umd-react package instead + if name in ("react", "react-dom") and extra_version.startswith( + "19." + ): + # Map filename to umd-react dist path + if "production.min" in filename: + umd_filename = f"{name}.production.min.js" + elif "development" in filename: + umd_filename = f"{name}.development.js" + else: + umd_filename = filename + url = f"https://unpkg.com/umd-react@{extra_version}/dist/{umd_filename}" + else: + url = f"https://unpkg.com/{name}@{extra_version}/umd/{filename}" + res = requests.get(url) + res.raise_for_status() extra_target = f"{name}@{extra_version}.{ext}" extra_path = self._concat(self.deps_folder, extra_target) with open(extra_path, "wb") as fp: @@ -169,7 +184,7 @@ def __init__(self): """dash-renderer's path is binding with the dash folder hierarchy.""" extras = [ "18.2.0", - "16.14.0", + "19.2.4", ] # versions to include beyond what's in package.json super().__init__( self._concat(os.path.dirname(__file__), os.pardir, "dash-renderer"),