From d1f038cdcdb4b991d945ff90ca6103bcd44716a5 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 11 Mar 2026 13:55:06 -0400 Subject: [PATCH 1/5] support react 19 --- dash/_dash_renderer.py | 44 ++++++++++++++++++++++++++----- dash/dash-renderer/init.template | 40 +++++++++++++++++++++++++--- dash/development/build_process.py | 16 +++++++++-- 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/dash/_dash_renderer.py b/dash/_dash_renderer.py index ee507ddb71..a9fcc68b0f 100644 --- a/dash/_dash_renderer.py +++ b/dash/_dash_renderer.py @@ -3,8 +3,8 @@ __version__ = "3.0.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.0"} +_available_reactdom_versions = {"18.3.1", "18.2.0", "19.2.0"} _js_dist_dependencies: List[Dict[str, Any]] = [] # to be set by _set_react_version @@ -19,19 +19,51 @@ 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 19+ removed UMD builds, use umd-react package instead + is_react19 = v_react.startswith("19.") + is_reactdom19 = v_reactdom.startswith("19.") + + if is_react19: + react_prod_url = ( + f"https://unpkg.com/umd-react@{v_react}/dist/react.production.min.js" + ) + react_dev_url = ( + f"https://unpkg.com/umd-react@{v_react}/dist/react.development.js" + ) + else: + react_prod_url = ( + f"https://unpkg.com/react@{v_react}/umd/react.production.min.js" + ) + react_dev_url = f"https://unpkg.com/react@{v_react}/umd/react.development.js" + + if is_reactdom19: + reactdom_prod_url = ( + f"https://unpkg.com/umd-react@{v_reactdom}/dist/react-dom.production.min.js" + ) + reactdom_dev_url = ( + f"https://unpkg.com/umd-react@{v_reactdom}/dist/react-dom.development.js" + ) + else: + reactdom_prod_url = ( + f"https://unpkg.com/react-dom@{v_reactdom}/umd/react-dom.production.min.js" + ) + reactdom_dev_url = ( + f"https://unpkg.com/react-dom@{v_reactdom}/umd/react-dom.development.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_prod_url, + reactdom_prod_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_dev_url, + reactdom_dev_url, "https://unpkg.com/prop-types@15.8.1/prop-types.js", ], }, diff --git a/dash/dash-renderer/init.template b/dash/dash-renderer/init.template index 463cfa02aa..12119dea23 100644 --- a/dash/dash-renderer/init.template +++ b/dash/dash-renderer/init.template @@ -19,19 +19,51 @@ 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 19+ removed UMD builds, use umd-react package instead + is_react19 = v_react.startswith("19.") + is_reactdom19 = v_reactdom.startswith("19.") + + if is_react19: + react_prod_url = ( + f"https://unpkg.com/umd-react@{v_react}/dist/react.production.min.js" + ) + react_dev_url = ( + f"https://unpkg.com/umd-react@{v_react}/dist/react.development.js" + ) + else: + react_prod_url = ( + f"https://unpkg.com/react@{v_react}/umd/react.production.min.js" + ) + react_dev_url = f"https://unpkg.com/react@{v_react}/umd/react.development.js" + + if is_reactdom19: + reactdom_prod_url = ( + f"https://unpkg.com/umd-react@{v_reactdom}/dist/react-dom.production.min.js" + ) + reactdom_dev_url = ( + f"https://unpkg.com/umd-react@{v_reactdom}/dist/react-dom.development.js" + ) + else: + reactdom_prod_url = ( + f"https://unpkg.com/react-dom@{v_reactdom}/umd/react-dom.production.min.js" + ) + reactdom_dev_url = ( + f"https://unpkg.com/react-dom@{v_reactdom}/umd/react-dom.development.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_prod_url, + reactdom_prod_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_dev_url, + reactdom_dev_url, "https://unpkg.com/prop-types@$proptypes/prop-types.js", ], }, diff --git a/dash/development/build_process.py b/dash/development/build_process.py index be65553716..81c184ebca 100644 --- a/dash/development/build_process.py +++ b/dash/development/build_process.py @@ -143,7 +143,19 @@ 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) extra_target = f"{name}@{extra_version}.{ext}" extra_path = self._concat(self.deps_folder, extra_target) @@ -169,7 +181,7 @@ def __init__(self): """dash-renderer's path is binding with the dash folder hierarchy.""" extras = [ "18.2.0", - "16.14.0", + "19.2.0", ] # versions to include beyond what's in package.json super().__init__( self._concat(os.path.dirname(__file__), os.pardir, "dash-renderer"), From de0a519281b0766406c2be64ee1f5bd2499d3de4 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 11 Mar 2026 14:18:10 -0400 Subject: [PATCH 2/5] fix lintstaged for python subdirectory files --- .lintstagedrc.js | 2 +- dash/development/build_process.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.lintstagedrc.js b/.lintstagedrc.js index e4449e59cd..c60351d080 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( " " )}`, diff --git a/dash/development/build_process.py b/dash/development/build_process.py index 81c184ebca..01c9a01086 100644 --- a/dash/development/build_process.py +++ b/dash/development/build_process.py @@ -144,7 +144,9 @@ def bundles(self, build=None): # pylint:disable=too-many-locals for extra_version in extras: # React 19+ removed UMD builds, use umd-react package instead - if name in ("react", "react-dom") and extra_version.startswith("19."): + 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" From 70ac64acaace156dc69857df0414c5dbd9d7a9a7 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 20 Apr 2026 10:44:00 -0400 Subject: [PATCH 3/5] Add react19 testing matrix --- .github/workflows/testing.yml | 36 ++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d0dee4bae0..0ac800fec3 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -452,7 +452,7 @@ jobs: retention-days: 7 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 @@ -460,11 +460,13 @@ jobs: fail-fast: false matrix: python-version: ["3.8", "3.12"] + react-version: ["18.3.1", "19.2.0"] 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 steps: @@ -526,7 +528,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 @@ -534,13 +536,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')) || @@ -551,10 +553,12 @@ jobs: fail-fast: false matrix: python-version: ["3.8", "3.12"] + react-version: ["18.3.1", "19.2.0"] 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 steps: @@ -615,7 +619,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 @@ -623,7 +627,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 @@ -718,7 +722,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')) || @@ -729,11 +733,13 @@ jobs: fail-fast: false matrix: python-version: ["3.8", "3.12"] + react-version: ["18.3.1", "19.2.0"] 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 steps: @@ -792,7 +798,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 @@ -800,7 +806,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 From 6410a027d36f93214f2287c0eb53bcc35c0056f9 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 20 Apr 2026 11:50:27 -0400 Subject: [PATCH 4/5] percy only for react 18 --- .github/workflows/testing.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0ac800fec3..4dae9d1e19 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -518,7 +518,7 @@ jobs: - name: Run main integration tests run: | - if [ "${{ matrix.python-version }}" == "3.12" ]; then + if [ "${{ matrix.python-version }}" == "3.12" ] && [ "${{ matrix.react-version }}" == "18.3.1" ]; then npx percy exec -- pytest --headless --nopercyfinalize --junitxml=test-reports/junit_intg.xml tests/integration --splits 3 --group ${{ matrix.test-group }} else pytest --headless --nopercyfinalize --junitxml=test-reports/junit_intg.xml tests/integration --splits 3 --group ${{ matrix.test-group }} @@ -609,7 +609,7 @@ jobs: working-directory: components/dash-html-components run: | npm ci - if [ "${{ matrix.python-version }}" == "3.12" ]; then + if [ "${{ matrix.python-version }}" == "3.12" ] && [ "${{ matrix.react-version }}" == "18.3.1" ]; then npx percy exec -- pytest --headless --nopercyfinalize --junitxml=test-reports/junit_html.xml else pytest --headless --nopercyfinalize --junitxml=test-reports/junit_html.xml From 1ab7182f1c20043c93b98a7d98699c9b8e509106 Mon Sep 17 00:00:00 2001 From: philippe Date: Mon, 20 Apr 2026 13:56:44 -0400 Subject: [PATCH 5/5] Add react 19 shim --- .../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/src/index.js | 1 + dash/dash-renderer/src/react19-shim.js | 87 +++++++++++++++++++ dash/dash-renderer/webpack.base.config.js | 2 + 6 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 dash/dash-renderer/src/react19-shim.js 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/src/index.js b/dash/dash-renderer/src/index.js index 3a2bbc7add..cfb8bd9a0d 100644 --- a/dash/dash-renderer/src/index.js +++ b/dash/dash-renderer/src/index.js @@ -1,3 +1,4 @@ +import './react19-shim'; // Must be first - React 19 compatibility import {DashRenderer} from './DashRenderer'; import './utils/clientsideFunctions'; diff --git a/dash/dash-renderer/src/react19-shim.js b/dash/dash-renderer/src/react19-shim.js new file mode 100644 index 0000000000..9ce951a9ba --- /dev/null +++ b/dash/dash-renderer/src/react19-shim.js @@ -0,0 +1,87 @@ +/** + * React 19 compatibility shim + * + * Provides compatibility for components bundled with older React versions + * when running with React 19: + * + * 1. ReactCurrentOwner stub - React 19 removed this from internals but some + * libraries still access it. + * + * 2. Global jsx-runtime - Provides jsx/jsxs functions using the current React + * version's createElement, ensuring element format compatibility. + * + * Must be imported before any component code. + */ +(function () { + if (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}; + } + + // Provide global jsx-runtime that uses current React version + // This ensures elements are created in the correct format for the loaded React + function jsx(type, config, maybeKey) { + var props = {}; + var children = null; + + // Copy props, extracting children and special props + if (config != null) { + // Handle key - include in props for createElement + 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]; + } + } + } + } + + // Handle key passed as third argument (overrides config.key) + if (maybeKey !== undefined) { + props.key = '' + maybeKey; + } + + // Call createElement with children as separate arguments + // Only pass children if they exist and are meaningful + 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 same as jsx but for static children + Fragment: React.Fragment + }; +})(); diff --git a/dash/dash-renderer/webpack.base.config.js b/dash/dash-renderer/webpack.base.config.js index ed95239f7d..046e0df6c8 100644 --- a/dash/dash-renderer/webpack.base.config.js +++ b/dash/dash-renderer/webpack.base.config.js @@ -67,6 +67,8 @@ const rendererOptions = { externals: { react: 'React', 'react-dom': 'ReactDOM', + 'react/jsx-runtime': 'ReactJSXRuntime', + 'react/jsx-dev-runtime': 'ReactJSXRuntime', 'prop-types': 'PropTypes' }, ...defaults