From 74a3041aeb59844a635d3dde1445d4b3e5bbf99a Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Thu, 22 Oct 2026 12:43:19 -0700 Subject: [PATCH 1/7] Render with createRoot() in renderWebChat() when ReactDOM.render() is unavailable --- .../react19/asset/reactDOMWithoutRender.js | 9 +++ .../html2/react19/renderWebChat.esm.html | 71 ++++++++++++++++++ .../react19/renderWebChat.esm.html.snap-1.png | Bin 0 -> 4464 bytes .../react19/renderWebChat.esm.html.snap-2.png | Bin 0 -> 7242 bytes .../html2/react19/renderWebChat.iife.html | 67 +++++++++++++++++ .../renderWebChat.iife.html.snap-1.png | Bin 0 -> 4464 bytes packages/bundle/esbuild.static.mjs | 12 +++ .../private/importReactDOMClient.static.ts | 9 +++ .../src/private/importReactDOMClient.ts | 10 +++ packages/bundle/src/renderWebChat.tsx | 68 ++++++++++++++++- packages/isomorphic-react-dom/src/ReactDOM.js | 5 +- 11 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 __tests__/html2/react19/asset/reactDOMWithoutRender.js create mode 100644 __tests__/html2/react19/renderWebChat.esm.html create mode 100644 __tests__/html2/react19/renderWebChat.esm.html.snap-1.png create mode 100644 __tests__/html2/react19/renderWebChat.esm.html.snap-2.png create mode 100644 __tests__/html2/react19/renderWebChat.iife.html create mode 100644 __tests__/html2/react19/renderWebChat.iife.html.snap-1.png create mode 100644 packages/bundle/src/private/importReactDOMClient.static.ts create mode 100644 packages/bundle/src/private/importReactDOMClient.ts diff --git a/__tests__/html2/react19/asset/reactDOMWithoutRender.js b/__tests__/html2/react19/asset/reactDOMWithoutRender.js new file mode 100644 index 0000000000..03bedf7469 --- /dev/null +++ b/__tests__/html2/react19/asset/reactDOMWithoutRender.js @@ -0,0 +1,9 @@ +// Emulates "react-dom" of React 19, where `ReactDOM.render()` was removed, on top of React 18. +// Named exports are kept: react-film, bundled in the static build, imports `render` by name (spyip/react-film#110). +import ReactDOM from '/__dist__/packages/bundle/static/react-dom.js'; + +export * from '/__dist__/packages/bundle/static/react-dom.js'; + +const { render: _render, ...ReactDOMWithoutRender } = ReactDOM; + +export default Object.freeze(ReactDOMWithoutRender); diff --git a/__tests__/html2/react19/renderWebChat.esm.html b/__tests__/html2/react19/renderWebChat.esm.html new file mode 100644 index 0000000000..7753bba6f8 --- /dev/null +++ b/__tests__/html2/react19/renderWebChat.esm.html @@ -0,0 +1,71 @@ + + + + renderWebChat() renders with createRoot() when ReactDOM.render() is unavailable (ESM build) + + + +
+ + + + + diff --git a/__tests__/html2/react19/renderWebChat.esm.html.snap-1.png b/__tests__/html2/react19/renderWebChat.esm.html.snap-1.png new file mode 100644 index 0000000000000000000000000000000000000000..041fcd25008f4b64afdaa84c2d68a5f265700be5 GIT binary patch literal 4464 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 A?*IS* literal 0 HcmV?d00001 diff --git a/__tests__/html2/react19/renderWebChat.iife.html b/__tests__/html2/react19/renderWebChat.iife.html new file mode 100644 index 0000000000..71466344f0 --- /dev/null +++ b/__tests__/html2/react19/renderWebChat.iife.html @@ -0,0 +1,67 @@ + + + + React 19: renderWebChat() of the IIFE build asks for createRoot() + + + + + + + + +
+ + + diff --git a/__tests__/html2/react19/renderWebChat.iife.html.snap-1.png b/__tests__/html2/react19/renderWebChat.iife.html.snap-1.png new file mode 100644 index 0000000000000000000000000000000000000000..041fcd25008f4b64afdaa84c2d68a5f265700be5 GIT binary patch literal 4464 zcmeI0X*gSX8^(hfidRkRQez3z>9pSI!e|>?g&JFpZImL2XcZAlbciJgQ!S&lj4Enx zMQss5EF~ni)@Yj=N|IU<8cT}^RY4;19^c>Vn)ma2_1iiBbFOp#=Q;O%|DNl~_|@4? zNkKya1Oh49U$MRl0!bl3AnBAt(!h#&9A+H^`UYfgZRs9Yx;o?8;2uaD+OU*r_P1IE zC)|e$*a2DCfEyXMDh`zhL2Q+BNi#!W!x%nbf{r<6YsP!BS zZ?eskyz4=7vgA!2ZQ8$ z-?MGFe=c_V)=-*e#LkdTa;soGrfhf2K|eg16*U6&BrTVqsUc@O1f+Gubo5&%Ii1AF zDAAybiy1fbf<{w}mlf{0=%Jf4tjXVUqP{Ko@5h!7g?i!-_?$@{k4v%d0t4(RR=#W)bvx=^>*~hB9Vk41&moRw5?(^z9vP*@zr|&JBlhvKHAV(LojX`1uyX#|h z$(wR0MNHlkOrh1_(W5~n;W7R4y4|109C#KF?2Nq#4VjVSKfGrJM(x6h%F| zxcs6r^4R>in+uVolepg3xm50>`siZF*eiXS+0DQH+#Io>#d`rdD34z(!qQ2!eW9&p z`1t;hjXi=%(-s5`M*o zj#etT;fY7K_DNDUZTPg~bQcrCZLJvA^&ae2J&(etQWSoBb*>|N>f=I zaeLm?4ty;ydy!riGZ``h-45$&T!IJof!X1{>n3Mb||!6cojht3#Jz7G{g_5RZcn9lCBjk z(oJ^_hP2*xb@-IXHh?`|kK7vnlUglKHsK>^p3GR0Mp~=I;~2e_Fu;eTXRRE!w*?Fh zYq>XOo3DSd#fJ~Ns%2<~kJi;rkeO#nO);pbz}AA(i}Vsthi`c!9AwG&5H#bbdA=^% z`L-cDj>{Em?q#LMSjZU6@Em(+J4=^h1`L?=hv%x_19A&tX%y3F~&iX_3u&l$yv z6%dE|QR`4=SGNZ)=DSsst|LA?z?)DKK*Fv|(z29a9mMuK0;- zo^5)kne)icA4Y*Zi~b~6gpwy?AoDb9IGxB$1PTZ7u#()Bl&_<`#EeZ;_20NvP(vbt zLz+<5HhU)3aMe8LA6py7uzn{13d~&pT_uk@!on~~#=R!t@;r07@GR_nz>7?q`^O+V zL-iVfEkDVpHb|Mu%k^^9O=SK{+Zfi_ZxA`#3)sEJ@QpDyQ;@7obUR_2$pU1<0E#2q zBnxY-xM2oVj!%>9!(-k(J+jOsAy!;^r9(3@e5z<|!7O@beaXWB*^`z1>Yj;>L5!R9PkU^Ea2r>oA`@{o(_KpFh&Qq3gXedAcs_SWZ%? zs3^D`){1lm-YZQQRPf=(iFh3@CJPU-Xnj~q)dN?PXY2incG+6-0CmE&t7X9`C`B$J`Jrvm4x@gKDyG@Q`lLP7YjnDEE=^O=slZ` z<7O2iaJ#JLfY1kHUJ>+8N7b5m^77x)?|bEgfZ>4p!7RWnYlljIbBXVJA57;h79VO- zz#Ppk))@b;lF9?v)YX2_Gfcu9s@ibxkR{QXnnsO=$MpSt3*o5et?eCv6f`c4Ke_>U zfjD?}Bskvv)beXO?G*fE{WVq)7|4HOJ!ISyk+cOEx5zNu`{|ST#4KafXse%9I8(9Kd24NkQcgH<;$L!q?B+)O t55tFMi4+JVkpwt@l?BXAWDklY16r4wj7kM-@PHMNy^XVV ({ + path: resolve(resolveDir, `${path}.static.ts`) + })); + } +}; + function createWatcherPlugin(name) { /** @type { import('esbuild').Plugin } */ return { @@ -82,6 +93,7 @@ const BASE_CONFIG = { metafile: true, plugins: [ cssPlugin, + importReactDOMClientPlugin, injectCSSPlugin({ ignoreCSSEntries: ['static/botframework-webchat/component.css'], stylesPlaceholder: bundleStyleContentPlaceholder diff --git a/packages/bundle/src/private/importReactDOMClient.static.ts b/packages/bundle/src/private/importReactDOMClient.static.ts new file mode 100644 index 0000000000..e6472f7e51 --- /dev/null +++ b/packages/bundle/src/private/importReactDOMClient.static.ts @@ -0,0 +1,9 @@ +/** + * Imports "react-dom/client" through the import map of the page. + * + * Replaces `importReactDOMClient.ts` in the static build. + */ +export default function importReactDOMClient(): Promise | undefined { + // @ts-expect-error The types of React DOM 16 do not have "react-dom/client". + return import('react-dom/client'); +} diff --git a/packages/bundle/src/private/importReactDOMClient.ts b/packages/bundle/src/private/importReactDOMClient.ts new file mode 100644 index 0000000000..aa7df19ee0 --- /dev/null +++ b/packages/bundle/src/private/importReactDOMClient.ts @@ -0,0 +1,10 @@ +/** + * Imports "react-dom/client" on demand, only in the static build, where the import map of the page resolves it. + * + * Other builds cannot import it: bundlers fail to build for React 16 and 17, which do not have "react-dom/client". + * + * @returns `undefined`, the static build replaces this file with `importReactDOMClient.static.ts`. + */ +export default function importReactDOMClient(): Promise | undefined { + return undefined; +} diff --git a/packages/bundle/src/renderWebChat.tsx b/packages/bundle/src/renderWebChat.tsx index a702bdd147..31f4c84cdf 100644 --- a/packages/bundle/src/renderWebChat.tsx +++ b/packages/bundle/src/renderWebChat.tsx @@ -1,6 +1,70 @@ -import React, { ComponentType } from 'react'; +import React, { type ComponentType, type ReactNode } from 'react'; import ReactDOM from 'react-dom'; +import { function_, is, object } from 'valibot'; + +import importReactDOMClient from './private/importReactDOMClient'; + +type Root = { render(children: ReactNode): void }; + +const createRootSchema = object({ createRoot: function_() }); +// Both `ReactDOM.render()` (React 18 or earlier) and `root.render()`. +const renderSchema = object({ render: function_() }); + +// `createRoot()` can only be called once per element, rendering again reuses its root. +const rootByElement = new WeakMap(); + +function createRootRequiredError(cause?: unknown): Error { + return new Error( + 'botframework-webchat: renderWebChat() needs createRoot() from "react-dom/client" on React 19 or later. Render with createRoot() instead, or add "react-dom/client" to the import map of the ESM build.', + { cause } + ); +} + +function renderWithCreateRoot(reactDOMClient: unknown, children: ReactNode, element: HTMLElement): void { + if (!is(createRootSchema, reactDOMClient)) { + throw createRootRequiredError(); + } + + let root = rootByElement.get(element); + + if (!root) { + const createdRoot = reactDOMClient.createRoot(element); + + if (!is(renderSchema, createdRoot)) { + throw new Error('botframework-webchat: createRoot() did not return a root.'); + } + + rootByElement.set(element, (root = createdRoot)); + } + + root.render(children); +} export default function renderWebChat(ReactWebChat: ComponentType, props: any, element: HTMLElement): void { - ReactDOM.render(, element); + const children = ; + + if (is(renderSchema, ReactDOM)) { + // React 16 to 18, and Preact: render as before. + // Also the IIFE build, which bundles React DOM 16.8.6 unless `window.ReactDOM` is set, + // and the static (ESM) build with the React 18 of its import map. + ReactDOM.render(children, element); + } else { + // React 19 or later: `ReactDOM.render()` was removed, `createRoot()` is only in "react-dom/client". + const reactDOMClientPromise = importReactDOMClient(); + + // npm and IIFE builds: they cannot import "react-dom/client" (bundlers would fail on React 16 and 17), + // the host must render with `createRoot()` itself. + if (!reactDOMClientPromise) { + throw createRootRequiredError(); + } + + // Static (ESM) build: "react-dom/client" is imported through the import map of the page. + // `renderWebChat()` has already returned when it settles: log errors instead of leaving an unhandled rejection. + reactDOMClientPromise + .then( + reactDOMClient => renderWithCreateRoot(reactDOMClient, children, element), + error => console.error(createRootRequiredError(error)) + ) + .catch(error => console.error(error)); + } } diff --git a/packages/isomorphic-react-dom/src/ReactDOM.js b/packages/isomorphic-react-dom/src/ReactDOM.js index 69b3b0d32a..1aac433ea2 100644 --- a/packages/isomorphic-react-dom/src/ReactDOM.js +++ b/packages/isomorphic-react-dom/src/ReactDOM.js @@ -1,3 +1,2 @@ -const ReactDOM = require('react-dom'); - -module.exports = typeof window.ReactDOM === 'undefined' ? ReactDOM : window.ReactDOM; +// The bundled React DOM is only evaluated when it is used: it cannot initialize with another React, such as React 19 in `window.React`. +module.exports = typeof window.ReactDOM === 'undefined' ? require('react-dom') : window.ReactDOM; From bd5c77eefdd18b1d7bdda032d73232a54f9b2cde Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Thu, 22 Oct 2026 13:01:41 -0700 Subject: [PATCH 2/7] Add the changelog fragment --- .changelog/5963-fix-render-webchat-without-render.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/5963-fix-render-webchat-without-render.md diff --git a/.changelog/5963-fix-render-webchat-without-render.md b/.changelog/5963-fix-render-webchat-without-render.md new file mode 100644 index 0000000000..3f0abb4e90 --- /dev/null +++ b/.changelog/5963-fix-render-webchat-without-render.md @@ -0,0 +1,3 @@ +### Fixed + +- Fixed `renderWebChat()` failing when `ReactDOM.render()` is unavailable, related to [#5552](https://github.com/microsoft/BotFramework-WebChat/issues/5552) and [#5850](https://github.com/microsoft/BotFramework-WebChat/issues/5850), in PR [#5963](https://github.com/microsoft/BotFramework-WebChat/pull/5963), by [@OEvgeny](https://github.com/OEvgeny) From d57b101de6c9a73fbc581773e280b050dca13f45 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Thu, 22 Oct 2026 14:22:45 -0700 Subject: [PATCH 3/7] Make renderWebChat() async: reject when createRoot() is unavailable importReactDOMClient() always returns a promise; renderWebChat() returns Promise and rejects with the createRoot() error instead of throwing synchronously or logging a failed import. React 16 to 18 still render synchronously. The IIFE test now also renders again with the send box on the same root. --- .../html2/react19/renderWebChat.iife.html | 32 +++++++++++++---- .../renderWebChat.iife.html.snap-2.png | Bin 0 -> 7242 bytes .../private/importReactDOMClient.static.ts | 2 +- .../src/private/importReactDOMClient.ts | 6 ++-- packages/bundle/src/renderWebChat.tsx | 34 ++++++++---------- 5 files changed, 44 insertions(+), 30 deletions(-) create mode 100644 __tests__/html2/react19/renderWebChat.iife.html.snap-2.png diff --git a/__tests__/html2/react19/renderWebChat.iife.html b/__tests__/html2/react19/renderWebChat.iife.html index 71466344f0..d7e4cc3328 100644 --- a/__tests__/html2/react19/renderWebChat.iife.html +++ b/__tests__/html2/react19/renderWebChat.iife.html @@ -6,6 +6,7 @@ diff --git a/__tests__/html2/react19/renderWebChat.iife.html.snap-2.png b/__tests__/html2/react19/renderWebChat.iife.html.snap-2.png new file mode 100644 index 0000000000000000000000000000000000000000..575a4020535deadab25efc4cc4760be5c5857345 GIT binary patch literal 7242 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 A?*IS* literal 0 HcmV?d00001 diff --git a/packages/bundle/src/private/importReactDOMClient.static.ts b/packages/bundle/src/private/importReactDOMClient.static.ts index e6472f7e51..9d0265b9a1 100644 --- a/packages/bundle/src/private/importReactDOMClient.static.ts +++ b/packages/bundle/src/private/importReactDOMClient.static.ts @@ -3,7 +3,7 @@ * * Replaces `importReactDOMClient.ts` in the static build. */ -export default function importReactDOMClient(): Promise | undefined { +export default function importReactDOMClient(): Promise { // @ts-expect-error The types of React DOM 16 do not have "react-dom/client". return import('react-dom/client'); } diff --git a/packages/bundle/src/private/importReactDOMClient.ts b/packages/bundle/src/private/importReactDOMClient.ts index aa7df19ee0..afe829e032 100644 --- a/packages/bundle/src/private/importReactDOMClient.ts +++ b/packages/bundle/src/private/importReactDOMClient.ts @@ -3,8 +3,8 @@ * * Other builds cannot import it: bundlers fail to build for React 16 and 17, which do not have "react-dom/client". * - * @returns `undefined`, the static build replaces this file with `importReactDOMClient.static.ts`. + * @returns A promise resolving to `undefined`, the static build replaces this file with `importReactDOMClient.static.ts`. */ -export default function importReactDOMClient(): Promise | undefined { - return undefined; +export default function importReactDOMClient(): Promise { + return Promise.resolve(undefined); } diff --git a/packages/bundle/src/renderWebChat.tsx b/packages/bundle/src/renderWebChat.tsx index 31f4c84cdf..520000fa0e 100644 --- a/packages/bundle/src/renderWebChat.tsx +++ b/packages/bundle/src/renderWebChat.tsx @@ -20,7 +20,14 @@ function createRootRequiredError(cause?: unknown): Error { ); } -function renderWithCreateRoot(reactDOMClient: unknown, children: ReactNode, element: HTMLElement): void { +async function renderWithCreateRoot(children: ReactNode, element: HTMLElement): Promise { + // Static (ESM) build: "react-dom/client" is imported through the import map of the page. + // npm and IIFE builds resolve to `undefined`: they cannot import "react-dom/client" (bundlers would fail on React 16 and 17), + // the host must render with `createRoot()` itself. + const reactDOMClient = await importReactDOMClient().catch(error => { + throw createRootRequiredError(error); + }); + if (!is(createRootSchema, reactDOMClient)) { throw createRootRequiredError(); } @@ -40,31 +47,20 @@ function renderWithCreateRoot(reactDOMClient: unknown, children: ReactNode, elem root.render(children); } -export default function renderWebChat(ReactWebChat: ComponentType, props: any, element: HTMLElement): void { +export default async function renderWebChat( + ReactWebChat: ComponentType, + props: any, + element: HTMLElement +): Promise { const children = ; if (is(renderSchema, ReactDOM)) { - // React 16 to 18, and Preact: render as before. + // React 16 to 18, and Preact: render synchronously as before, the returned promise is already resolved. // Also the IIFE build, which bundles React DOM 16.8.6 unless `window.ReactDOM` is set, // and the static (ESM) build with the React 18 of its import map. ReactDOM.render(children, element); } else { // React 19 or later: `ReactDOM.render()` was removed, `createRoot()` is only in "react-dom/client". - const reactDOMClientPromise = importReactDOMClient(); - - // npm and IIFE builds: they cannot import "react-dom/client" (bundlers would fail on React 16 and 17), - // the host must render with `createRoot()` itself. - if (!reactDOMClientPromise) { - throw createRootRequiredError(); - } - - // Static (ESM) build: "react-dom/client" is imported through the import map of the page. - // `renderWebChat()` has already returned when it settles: log errors instead of leaving an unhandled rejection. - reactDOMClientPromise - .then( - reactDOMClient => renderWithCreateRoot(reactDOMClient, children, element), - error => console.error(createRootRequiredError(error)) - ) - .catch(error => console.error(error)); + await renderWithCreateRoot(children, element); } } From d2b9d8716201e86d7a976babb878ea24fac4011e Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Fri, 23 Oct 2026 11:26:04 -0700 Subject: [PATCH 4/7] renderWebChat(props, elementOrRoot): render with the host's root, never import react-dom/client Ported from 92bbd36e2 and d44517f4d. A DOM container (element, document, document fragment, by nodeType, checked first: custom elements such as Lit have their own render()) keeps ReactDOM.render() on React 16 to 18 and Preact; on React 19 it throws, asking for createRoot(element) instead. An object with render() is rendered with root.render() on any React version. Removes importReactDOMClient (and its static variant and esbuild plugin), createRootSchema and rootByElement: Web Chat no longer imports "react-dom/client" in any build. renderWebChat() is synchronous again and returns void, as in the last release. --- packages/bundle/esbuild.static.mjs | 12 --- .../src/boot/actual/renderWebChat/full.ts | 4 +- .../src/boot/actual/renderWebChat/minimal.ts | 4 +- .../private/importReactDOMClient.static.ts | 9 -- .../src/private/importReactDOMClient.ts | 10 -- packages/bundle/src/renderWebChat.spec.tsx | 59 +++++++++++ packages/bundle/src/renderWebChat.tsx | 99 +++++++++---------- 7 files changed, 109 insertions(+), 88 deletions(-) delete mode 100644 packages/bundle/src/private/importReactDOMClient.static.ts delete mode 100644 packages/bundle/src/private/importReactDOMClient.ts create mode 100644 packages/bundle/src/renderWebChat.spec.tsx diff --git a/packages/bundle/esbuild.static.mjs b/packages/bundle/esbuild.static.mjs index a725dbd26c..926bf32d9b 100644 --- a/packages/bundle/esbuild.static.mjs +++ b/packages/bundle/esbuild.static.mjs @@ -38,17 +38,6 @@ const isomorphicReactPlugin = { } }; -// "react-dom/client" is resolved by the import map of the page: only the static build can import it on demand. -const importReactDOMClientPlugin = { - name: 'import-react-dom-client', - setup(build) { - // eslint-disable-next-line require-unicode-regexp - build.onResolve({ filter: /\/private\/importReactDOMClient$/ }, ({ path, resolveDir }) => ({ - path: resolve(resolveDir, `${path}.static.ts`) - })); - } -}; - function createWatcherPlugin(name) { /** @type { import('esbuild').Plugin } */ return { @@ -93,7 +82,6 @@ const BASE_CONFIG = { metafile: true, plugins: [ cssPlugin, - importReactDOMClientPlugin, injectCSSPlugin({ ignoreCSSEntries: ['static/botframework-webchat/component.css'], stylesPlaceholder: bundleStyleContentPlaceholder diff --git a/packages/bundle/src/boot/actual/renderWebChat/full.ts b/packages/bundle/src/boot/actual/renderWebChat/full.ts index 03cfdbedea..e36b4fe0d0 100644 --- a/packages/bundle/src/boot/actual/renderWebChat/full.ts +++ b/packages/bundle/src/boot/actual/renderWebChat/full.ts @@ -1,6 +1,6 @@ -import coreRenderWebChat from '../../../renderWebChat'; +import coreRenderWebChat, { type RenderWebChat } from '../../../renderWebChat'; import { ReactWebChat } from '../component/full'; -const renderWebChat = coreRenderWebChat.bind(null, ReactWebChat); +const renderWebChat: RenderWebChat = coreRenderWebChat.bind(null, ReactWebChat); export { renderWebChat }; diff --git a/packages/bundle/src/boot/actual/renderWebChat/minimal.ts b/packages/bundle/src/boot/actual/renderWebChat/minimal.ts index e21586847c..f190c722c3 100644 --- a/packages/bundle/src/boot/actual/renderWebChat/minimal.ts +++ b/packages/bundle/src/boot/actual/renderWebChat/minimal.ts @@ -1,6 +1,6 @@ -import coreRenderWebChat from '../../../renderWebChat'; +import coreRenderWebChat, { type RenderWebChat } from '../../../renderWebChat'; import { ReactWebChat } from '../component/minimal'; -const renderWebChat = coreRenderWebChat.bind(null, ReactWebChat); +const renderWebChat: RenderWebChat = coreRenderWebChat.bind(null, ReactWebChat); export { renderWebChat }; diff --git a/packages/bundle/src/private/importReactDOMClient.static.ts b/packages/bundle/src/private/importReactDOMClient.static.ts deleted file mode 100644 index 9d0265b9a1..0000000000 --- a/packages/bundle/src/private/importReactDOMClient.static.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Imports "react-dom/client" through the import map of the page. - * - * Replaces `importReactDOMClient.ts` in the static build. - */ -export default function importReactDOMClient(): Promise { - // @ts-expect-error The types of React DOM 16 do not have "react-dom/client". - return import('react-dom/client'); -} diff --git a/packages/bundle/src/private/importReactDOMClient.ts b/packages/bundle/src/private/importReactDOMClient.ts deleted file mode 100644 index afe829e032..0000000000 --- a/packages/bundle/src/private/importReactDOMClient.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Imports "react-dom/client" on demand, only in the static build, where the import map of the page resolves it. - * - * Other builds cannot import it: bundlers fail to build for React 16 and 17, which do not have "react-dom/client". - * - * @returns A promise resolving to `undefined`, the static build replaces this file with `importReactDOMClient.static.ts`. - */ -export default function importReactDOMClient(): Promise { - return Promise.resolve(undefined); -} diff --git a/packages/bundle/src/renderWebChat.spec.tsx b/packages/bundle/src/renderWebChat.spec.tsx new file mode 100644 index 0000000000..89f44d65f0 --- /dev/null +++ b/packages/bundle/src/renderWebChat.spec.tsx @@ -0,0 +1,59 @@ +/** @jest-environment @happy-dom/jest-environment */ +/// + +import ReactDOM from 'react-dom'; + +import renderWebChat from './renderWebChat'; + +// `render` is set before each test, the React 19 test removes it. +jest.mock('react-dom', () => ({ __esModule: true, default: {} })); + +const ReactWebChat = () => null; + +let render: jest.Mock; + +beforeEach(() => { + render = jest.fn(); + Object.assign(ReactDOM, { render }); +}); + +test('Element should be rendered by ReactDOM.render()', () => { + const element = document.createElement('div'); + + renderWebChat(ReactWebChat, {}, element); + + expect(render).toHaveBeenCalledTimes(1); + expect(render.mock.calls[0][1]).toBe(element); +}); + +test('DocumentFragment should be rendered by ReactDOM.render() as a container', () => { + const fragment = document.createDocumentFragment(); + + renderWebChat(ReactWebChat, {}, fragment as unknown as HTMLElement); + + expect(render).toHaveBeenCalledTimes(1); + expect(render.mock.calls[0][1]).toBe(fragment); +}); + +test('Root should be rendered by root.render() instead of ReactDOM.render()', () => { + const root = { render: jest.fn() }; + + renderWebChat(ReactWebChat, {}, root); + + expect(root.render).toHaveBeenCalledTimes(1); + expect(render).not.toHaveBeenCalled(); +}); + +test('Element on React 19 should throw an error asking for a root', () => { + // React 19 removed `ReactDOM.render()`. + Object.assign(ReactDOM, { render: undefined }); + + expect(() => renderWebChat(ReactWebChat, {}, document.createElement('div'))).toThrow( + 'renderWebChat() on React 19 or later needs a root: pass createRoot(element) from "react-dom/client" instead of the element.' + ); +}); + +test('Neither a container nor a root should throw', () => + expect(() => renderWebChat(ReactWebChat, {}, {} as unknown as HTMLElement)).toThrow( + 'renderWebChat() expects an HTMLElement or a root from createRoot().' + )); diff --git a/packages/bundle/src/renderWebChat.tsx b/packages/bundle/src/renderWebChat.tsx index 520000fa0e..4b38ca5c10 100644 --- a/packages/bundle/src/renderWebChat.tsx +++ b/packages/bundle/src/renderWebChat.tsx @@ -1,66 +1,59 @@ import React, { type ComponentType, type ReactNode } from 'react'; import ReactDOM from 'react-dom'; -import { function_, is, object } from 'valibot'; +import { custom, function_, is, object, picklist } from 'valibot'; + +/** A root from `createRoot()` of "react-dom/client". */ +type RenderWebChatRoot = { render(children: ReactNode): void }; + +type RenderWebChat = { + /** + * Renders Web Chat into an element, or with a root from `createRoot()`. + * + * @param props Props of ``. + * @param elementOrRoot Element to render Web Chat into with `ReactDOM.render()`, or a root from `createRoot()` of "react-dom/client" to render Web Chat with. + * React 19 or later removed `ReactDOM.render()`: pass `createRoot(element)` instead of the element. + * @throws When an element is passed on React 19 or later, or when `elementOrRoot` is neither an element nor a root. + */ + (props: any, elementOrRoot: HTMLElement | RenderWebChatRoot): void; +}; + +// Same as `Node.ELEMENT_NODE`, `Node.DOCUMENT_NODE` and `Node.DOCUMENT_FRAGMENT_NODE`, without needing `Node` on import. +const ELEMENT_NODE = 1; +const DOCUMENT_NODE = 9; +const DOCUMENT_FRAGMENT_NODE = 11; + +// Containers `ReactDOM.render()` accepts: element, document and document fragment, by `nodeType` to work across realms. +// Checked before `renderSchema`: some custom elements (e.g. Lit) have their own `render()` method. +const containerSchema = custom(value => + is(object({ nodeType: picklist([DOCUMENT_FRAGMENT_NODE, DOCUMENT_NODE, ELEMENT_NODE]) }), value) +); -import importReactDOMClient from './private/importReactDOMClient'; - -type Root = { render(children: ReactNode): void }; - -const createRootSchema = object({ createRoot: function_() }); // Both `ReactDOM.render()` (React 18 or earlier) and `root.render()`. const renderSchema = object({ render: function_() }); -// `createRoot()` can only be called once per element, rendering again reuses its root. -const rootByElement = new WeakMap(); - -function createRootRequiredError(cause?: unknown): Error { - return new Error( - 'botframework-webchat: renderWebChat() needs createRoot() from "react-dom/client" on React 19 or later. Render with createRoot() instead, or add "react-dom/client" to the import map of the ESM build.', - { cause } - ); -} - -async function renderWithCreateRoot(children: ReactNode, element: HTMLElement): Promise { - // Static (ESM) build: "react-dom/client" is imported through the import map of the page. - // npm and IIFE builds resolve to `undefined`: they cannot import "react-dom/client" (bundlers would fail on React 16 and 17), - // the host must render with `createRoot()` itself. - const reactDOMClient = await importReactDOMClient().catch(error => { - throw createRootRequiredError(error); - }); - - if (!is(createRootSchema, reactDOMClient)) { - throw createRootRequiredError(); - } - - let root = rootByElement.get(element); - - if (!root) { - const createdRoot = reactDOMClient.createRoot(element); - - if (!is(renderSchema, createdRoot)) { - throw new Error('botframework-webchat: createRoot() did not return a root.'); - } - - rootByElement.set(element, (root = createdRoot)); - } - - root.render(children); -} - -export default async function renderWebChat( +export default function renderWebChat( ReactWebChat: ComponentType, props: any, - element: HTMLElement -): Promise { + elementOrRoot: HTMLElement | RenderWebChatRoot +): void { const children = ; - if (is(renderSchema, ReactDOM)) { - // React 16 to 18, and Preact: render synchronously as before, the returned promise is already resolved. - // Also the IIFE build, which bundles React DOM 16.8.6 unless `window.ReactDOM` is set, - // and the static (ESM) build with the React 18 of its import map. - ReactDOM.render(children, element); + if (is(containerSchema, elementOrRoot)) { + // React 19 or later removed `ReactDOM.render()`: Web Chat does not import "react-dom/client", the host creates the root. + if (!is(renderSchema, ReactDOM)) { + throw new Error( + 'botframework-webchat: renderWebChat() on React 19 or later needs a root: pass createRoot(element) from "react-dom/client" instead of the element.' + ); + } + + // React 16 to 18, and Preact. Also the IIFE build, which bundles React DOM 16.8.6 unless `window.ReactDOM` is set. + ReactDOM.render(children, elementOrRoot); + } else if (is(renderSchema, elementOrRoot)) { + // The host created the root: render with it on any React version, it is the host's to unmount. + elementOrRoot.render(children); } else { - // React 19 or later: `ReactDOM.render()` was removed, `createRoot()` is only in "react-dom/client". - await renderWithCreateRoot(children, element); + throw new Error('botframework-webchat: renderWebChat() expects an HTMLElement or a root from createRoot().'); } } + +export { type RenderWebChat, type RenderWebChatRoot }; From 0a8865802c43579f0b5397a4d1609aaf529f8c69 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Fri, 23 Oct 2026 11:28:41 -0700 Subject: [PATCH 5/7] Test renderWebChat() with the host's root on React 19 (IIFE, ESM) and React 18, and a custom element with render() Ported from 2fb2d045e: the pages create the root with createRoot() from "react-dom/client" of their import map and pass it; an element on React 19 throws, asking for a root. The IIFE test calls renderWebChat() again on the same root with { hideSendBox: false }. --- ...nderWebChat.customElement.react18.esm.html | 72 ++++++++++++++ ....customElement.react18.esm.html.snap-1.png | Bin 0 -> 4464 bytes .../html2/react19/renderWebChat.esm.html | 21 +++-- .../html2/react19/renderWebChat.iife.html | 34 ++++--- .../renderWebChat.root.react18.esm.html | 88 ++++++++++++++++++ ...erWebChat.root.react18.esm.html.snap-1.png | Bin 0 -> 4464 bytes 6 files changed, 198 insertions(+), 17 deletions(-) create mode 100644 __tests__/html2/react19/renderWebChat.customElement.react18.esm.html create mode 100644 __tests__/html2/react19/renderWebChat.customElement.react18.esm.html.snap-1.png create mode 100644 __tests__/html2/react19/renderWebChat.root.react18.esm.html create mode 100644 __tests__/html2/react19/renderWebChat.root.react18.esm.html.snap-1.png diff --git a/__tests__/html2/react19/renderWebChat.customElement.react18.esm.html b/__tests__/html2/react19/renderWebChat.customElement.react18.esm.html new file mode 100644 index 0000000000..859a74e2fa --- /dev/null +++ b/__tests__/html2/react19/renderWebChat.customElement.react18.esm.html @@ -0,0 +1,72 @@ + + + + renderWebChat() renders into a custom element that has its own render() method (ESM build) + + + +
+ + +
+ + + + diff --git a/__tests__/html2/react19/renderWebChat.customElement.react18.esm.html.snap-1.png b/__tests__/html2/react19/renderWebChat.customElement.react18.esm.html.snap-1.png new file mode 100644 index 0000000000000000000000000000000000000000..041fcd25008f4b64afdaa84c2d68a5f265700be5 GIT binary patch literal 4464 zcmeI0X*gSX8^(hfidRkRQez3z>9pSI!e|>?g&JFpZImL2XcZAlbciJgQ!S&lj4Enx zMQss5EF~ni)@Yj=N|IU<8cT}^RY4;19^c>Vn)ma2_1iiBbFOp#=Q;O%|DNl~_|@4? zNkKya1Oh49U$MRl0!bl3AnBAt(!h#&9A+H^`UYfgZRs9Yx;o?8;2uaD+OU*r_P1IE zC)|e$*a2DCfEyXMDh`zhL2Q+BNi#!W!x%nbf{r<6YsP!BS zZ?eskyz4=7vgA!2ZQ8$ z-?MGFe=c_V)=-*e#LkdTa;soGrfhf2K|eg16*U6&BrTVqsUc@O1f+Gubo5&%Ii1AF zDAAybiy1fbf<{w}mlf{0=%Jf4tjXVUqP{Ko@5h!7g?i!-_?$@{k4v%d0t4(RR=#W)bvx=^>*~hB9Vk41&moRw5?(^z9vP*@zr|&JBlhvKHAV(LojX`1uyX#|h z$(wR0MNHlkOrh1_(W5~n;W7R4y4|109C#KF?2Nq#4VjVSKfGrJM(x6h%F| zxcs6r^4R>in+uVolepg3xm50>`siZF*eiXS+0DQH+#Io>#d`rdD34z(!qQ2!eW9&p z`1t;hjXi=%(-s5`M*o zj#etT;fY7K_DNDUZTPg~bQcrCZLJvA^&ae2J&(etQWSoBb*>|N>f=I zaeLm?4ty;ydy!riGZ``h-45$&T!IJof!X1{>n3Mb||!6cojht3#Jz7G{g_5RZcn9lCBjk z(oJ^_hP2*xb@-IXHh?`|kK7vnlUglKHsK>^p3GR0Mp~=I;~2e_Fu;eTXRRE!w*?Fh zYq>XOo3DSd#fJ~Ns%2<~kJi;rkeO#nO);pbz}AA(i}Vsthi`c!9AwG&5H#bbdA=^% z`L-cDj>{Em?q#LMSjZU6@Em(+J4=^h1`L?=hv%x_19A&tX%y3F~&iX_3u&l$yv z6%dE|QR`4=SGNZ)=DSsst|LA?z?)DKK*Fv|(z29a9mMuK0;- zo^5)kne)icA4Y*Zi~b~6gpwy?AoDb9IGxB$1PTZ7u#()Bl&_<`#EeZ;_20NvP(vbt zLz+<5HhU)3aMe8LA6py7uzn{13d~&pT_uk@!on~~#=R!t@;r07@GR_nz>7?q`^O+V zL-iVfEkDVpHb|Mu%k^^9O=SK{+Zfi_ZxA`#3)sEJ@QpDyQ;@7obUR_2$pU1<0E#2q zBnxY-xM2oVj!%>9!(-k(J+jOsAy!;^r9(3@e5z<|!7O@beaXWB*^`z1>Yj;>L5!R9PkU^Ea2r>oA`@{o(_KpFh&Qq3gXedAcs_SWZ%? zs3^D`){1lm-YZQQRPf=(iFh3@CJPU-Xnj~q)dN?PXY2incG+6-0CmE&t7X9`C`B$J`Jrvm4x@gKDyG@Q`lLP7YjnDEE=^O=slZ` z<7O2iaJ#JLfY1kHUJ>+8N7b5m^77x)?|bEgfZ>4p!7RWnYlljIbBXVJA57;h79VO- zz#Ppk))@b;lF9?v)YX2_Gfcu9s@ibxkR{QXnnsO=$MpSt3*o5et?eCv6f`c4Ke_>U zfjD?}Bskvv)beXO?G*fE{WVq)7|4HOJ!ISyk+cOEx5zNu`{|ST#4KafXse%9I8(9Kd24NkQcgH<;$L!q?B+)O t55tFMi4+JVkpwt@l?BXAWDklY16r4wj7kM-@PHMNy^XVV - renderWebChat() renders with createRoot() when ReactDOM.render() is unavailable (ESM build) + renderWebChat() renders with the root passed when ReactDOM.render() is unavailable (ESM build) @@ -28,20 +28,29 @@ import { waitFor } from '@testduet/wait-for'; import { createStoreWithOptions, renderWebChat, testIds } from 'botframework-webchat'; import ReactDOM from 'react-dom'; + import { createRoot } from 'react-dom/client'; run(async function () { // TODO: This is for `createDirectLineEmulator` and `pageElements` only, should find ways to eliminate this line. window.WebChat = { createStoreWithOptions, testIds }; const { directLine, store } = testHelpers.createDirectLineEmulator(); + const webChatElement = document.getElementById('webchat'); // GIVEN: "react-dom" without `ReactDOM.render()`, like React 19. expect(ReactDOM.render).toBeUndefined(); - // WHEN: Web Chat is rendered by renderWebChat() without the send box. - renderWebChat({ directLine, store, styleOptions: { hideSendBox: true } }, document.getElementById('webchat')); + // WHEN: Web Chat is rendered by renderWebChat() into the element. + // THEN: It throws, asking for a root: the page imports "react-dom/client", not Web Chat. + expect(() => renderWebChat({ directLine, store }, webChatElement)).toThrow( + 'renderWebChat() on React 19 or later needs a root: pass createRoot(element) from "react-dom/client" instead of the element.' + ); + + // WHEN: Web Chat is rendered by renderWebChat() with a root from createRoot() of the page, without the send box. + const root = createRoot(webChatElement); + + renderWebChat({ directLine, store, styleOptions: { hideSendBox: true } }, root); - // THEN: Web Chat is rendered through createRoot() from "react-dom/client", the only way left. await pageConditions.uiConnected(); await directLine.emulateIncomingActivity('Hello, World!'); @@ -54,8 +63,8 @@ const transcript = pageElements.transcript(); - // WHEN: renderWebChat() is called again on the same element, with the send box. - renderWebChat({ directLine, store, styleOptions: { hideSendBox: false } }, document.getElementById('webchat')); + // WHEN: renderWebChat() is called again with the same root, with the send box. + renderWebChat({ directLine, store, styleOptions: { hideSendBox: false } }, root); // THEN: The send box appears. await waitFor(() => expect(pageElements.sendBoxTextBox()).toBeTruthy()); diff --git a/__tests__/html2/react19/renderWebChat.iife.html b/__tests__/html2/react19/renderWebChat.iife.html index d7e4cc3328..221d62ab38 100644 --- a/__tests__/html2/react19/renderWebChat.iife.html +++ b/__tests__/html2/react19/renderWebChat.iife.html @@ -1,7 +1,7 @@ - React 19: renderWebChat() of the IIFE build asks for createRoot() + React 19: renderWebChat() of the IIFE build renders with the root passed diff --git a/__tests__/html2/react19/renderWebChat.root.react18.esm.html b/__tests__/html2/react19/renderWebChat.root.react18.esm.html new file mode 100644 index 0000000000..f11837f58d --- /dev/null +++ b/__tests__/html2/react19/renderWebChat.root.react18.esm.html @@ -0,0 +1,88 @@ + + + + React 18: renderWebChat() renders with the root passed instead of ReactDOM.render() (ESM build) + + + +
+ + + + diff --git a/__tests__/html2/react19/renderWebChat.root.react18.esm.html.snap-1.png b/__tests__/html2/react19/renderWebChat.root.react18.esm.html.snap-1.png new file mode 100644 index 0000000000000000000000000000000000000000..041fcd25008f4b64afdaa84c2d68a5f265700be5 GIT binary patch literal 4464 zcmeI0X*gSX8^(hfidRkRQez3z>9pSI!e|>?g&JFpZImL2XcZAlbciJgQ!S&lj4Enx zMQss5EF~ni)@Yj=N|IU<8cT}^RY4;19^c>Vn)ma2_1iiBbFOp#=Q;O%|DNl~_|@4? zNkKya1Oh49U$MRl0!bl3AnBAt(!h#&9A+H^`UYfgZRs9Yx;o?8;2uaD+OU*r_P1IE zC)|e$*a2DCfEyXMDh`zhL2Q+BNi#!W!x%nbf{r<6YsP!BS zZ?eskyz4=7vgA!2ZQ8$ z-?MGFe=c_V)=-*e#LkdTa;soGrfhf2K|eg16*U6&BrTVqsUc@O1f+Gubo5&%Ii1AF zDAAybiy1fbf<{w}mlf{0=%Jf4tjXVUqP{Ko@5h!7g?i!-_?$@{k4v%d0t4(RR=#W)bvx=^>*~hB9Vk41&moRw5?(^z9vP*@zr|&JBlhvKHAV(LojX`1uyX#|h z$(wR0MNHlkOrh1_(W5~n;W7R4y4|109C#KF?2Nq#4VjVSKfGrJM(x6h%F| zxcs6r^4R>in+uVolepg3xm50>`siZF*eiXS+0DQH+#Io>#d`rdD34z(!qQ2!eW9&p z`1t;hjXi=%(-s5`M*o zj#etT;fY7K_DNDUZTPg~bQcrCZLJvA^&ae2J&(etQWSoBb*>|N>f=I zaeLm?4ty;ydy!riGZ``h-45$&T!IJof!X1{>n3Mb||!6cojht3#Jz7G{g_5RZcn9lCBjk z(oJ^_hP2*xb@-IXHh?`|kK7vnlUglKHsK>^p3GR0Mp~=I;~2e_Fu;eTXRRE!w*?Fh zYq>XOo3DSd#fJ~Ns%2<~kJi;rkeO#nO);pbz}AA(i}Vsthi`c!9AwG&5H#bbdA=^% z`L-cDj>{Em?q#LMSjZU6@Em(+J4=^h1`L?=hv%x_19A&tX%y3F~&iX_3u&l$yv z6%dE|QR`4=SGNZ)=DSsst|LA?z?)DKK*Fv|(z29a9mMuK0;- zo^5)kne)icA4Y*Zi~b~6gpwy?AoDb9IGxB$1PTZ7u#()Bl&_<`#EeZ;_20NvP(vbt zLz+<5HhU)3aMe8LA6py7uzn{13d~&pT_uk@!on~~#=R!t@;r07@GR_nz>7?q`^O+V zL-iVfEkDVpHb|Mu%k^^9O=SK{+Zfi_ZxA`#3)sEJ@QpDyQ;@7obUR_2$pU1<0E#2q zBnxY-xM2oVj!%>9!(-k(J+jOsAy!;^r9(3@e5z<|!7O@beaXWB*^`z1>Yj;>L5!R9PkU^Ea2r>oA`@{o(_KpFh&Qq3gXedAcs_SWZ%? zs3^D`){1lm-YZQQRPf=(iFh3@CJPU-Xnj~q)dN?PXY2incG+6-0CmE&t7X9`C`B$J`Jrvm4x@gKDyG@Q`lLP7YjnDEE=^O=slZ` z<7O2iaJ#JLfY1kHUJ>+8N7b5m^77x)?|bEgfZ>4p!7RWnYlljIbBXVJA57;h79VO- zz#Ppk))@b;lF9?v)YX2_Gfcu9s@ibxkR{QXnnsO=$MpSt3*o5et?eCv6f`c4Ke_>U zfjD?}Bskvv)beXO?G*fE{WVq)7|4HOJ!ISyk+cOEx5zNu`{|ST#4KafXse%9I8(9Kd24NkQcgH<;$L!q?B+)O t55tFMi4+JVkpwt@l?BXAWDklY16r4wj7kM-@PHMNy^XVV Date: Fri, 23 Oct 2026 11:30:42 -0700 Subject: [PATCH 6/7] Document passing a root from createRoot() to renderWebChat() for React 19, update the changelog fragment --- .changelog/5963-fix-render-webchat-without-render.md | 4 ++-- docs/HOSTING.md | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.changelog/5963-fix-render-webchat-without-render.md b/.changelog/5963-fix-render-webchat-without-render.md index 3f0abb4e90..661566c34c 100644 --- a/.changelog/5963-fix-render-webchat-without-render.md +++ b/.changelog/5963-fix-render-webchat-without-render.md @@ -1,3 +1,3 @@ -### Fixed +### Added -- Fixed `renderWebChat()` failing when `ReactDOM.render()` is unavailable, related to [#5552](https://github.com/microsoft/BotFramework-WebChat/issues/5552) and [#5850](https://github.com/microsoft/BotFramework-WebChat/issues/5850), in PR [#5963](https://github.com/microsoft/BotFramework-WebChat/pull/5963), by [@OEvgeny](https://github.com/OEvgeny) +- `renderWebChat()`: added accepting a root from `createRoot()`, for React 19, related to [#5552](https://github.com/microsoft/BotFramework-WebChat/issues/5552) and [#5850](https://github.com/microsoft/BotFramework-WebChat/issues/5850), in PR [#5963](https://github.com/microsoft/BotFramework-WebChat/pull/5963), by [@OEvgeny](https://github.com/OEvgeny) diff --git a/docs/HOSTING.md b/docs/HOSTING.md index e7e566c98a..993f092618 100644 --- a/docs/HOSTING.md +++ b/docs/HOSTING.md @@ -230,7 +230,13 @@ Then, run the following JavaScript code to render Web Chat into the placeholder. renderWebChat({ directLine }, document.getElementById('webchat')); ``` -> Note: The `renderWebChat()` function is a simple wrapper around `render()` function in React DOM. +> Note: The `renderWebChat()` function is a simple wrapper around `render()` function in React DOM. React 19 removed `render()`: create a root with `createRoot()` from `react-dom/client` and pass it instead of the element. + +```tsx +import { createRoot } from 'react-dom/client'; + +renderWebChat({ directLine }, createRoot(document.getElementById('webchat'))); +``` ### Using `` component From 301f8e1e7105257e6a472c0ce5cfbdc9fffbc80c Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Fri, 23 Oct 2026 11:44:41 -0700 Subject: [PATCH 7/7] Type renderWebChat()'s container like ReactDOM.render() (Element | Document | DocumentFragment | null) or a root --- .changelog/5963-fix-render-webchat-without-render.md | 2 +- packages/bundle/src/renderWebChat.spec.tsx | 2 +- packages/bundle/src/renderWebChat.tsx | 11 +++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.changelog/5963-fix-render-webchat-without-render.md b/.changelog/5963-fix-render-webchat-without-render.md index 661566c34c..c54e7a0e76 100644 --- a/.changelog/5963-fix-render-webchat-without-render.md +++ b/.changelog/5963-fix-render-webchat-without-render.md @@ -1,3 +1,3 @@ ### Added -- `renderWebChat()`: added accepting a root from `createRoot()`, for React 19, related to [#5552](https://github.com/microsoft/BotFramework-WebChat/issues/5552) and [#5850](https://github.com/microsoft/BotFramework-WebChat/issues/5850), in PR [#5963](https://github.com/microsoft/BotFramework-WebChat/pull/5963), by [@OEvgeny](https://github.com/OEvgeny) +- `renderWebChat()`: added support of a root from `createRoot()` for React 19, related to [#5552](https://github.com/microsoft/BotFramework-WebChat/issues/5552) and [#5850](https://github.com/microsoft/BotFramework-WebChat/issues/5850), in PR [#5963](https://github.com/microsoft/BotFramework-WebChat/pull/5963), by [@OEvgeny](https://github.com/OEvgeny) diff --git a/packages/bundle/src/renderWebChat.spec.tsx b/packages/bundle/src/renderWebChat.spec.tsx index 89f44d65f0..1ca156f321 100644 --- a/packages/bundle/src/renderWebChat.spec.tsx +++ b/packages/bundle/src/renderWebChat.spec.tsx @@ -29,7 +29,7 @@ test('Element should be rendered by ReactDOM.render()', () => { test('DocumentFragment should be rendered by ReactDOM.render() as a container', () => { const fragment = document.createDocumentFragment(); - renderWebChat(ReactWebChat, {}, fragment as unknown as HTMLElement); + renderWebChat(ReactWebChat, {}, fragment); expect(render).toHaveBeenCalledTimes(1); expect(render.mock.calls[0][1]).toBe(fragment); diff --git a/packages/bundle/src/renderWebChat.tsx b/packages/bundle/src/renderWebChat.tsx index 4b38ca5c10..d1e9230573 100644 --- a/packages/bundle/src/renderWebChat.tsx +++ b/packages/bundle/src/renderWebChat.tsx @@ -5,6 +5,9 @@ import { custom, function_, is, object, picklist } from 'valibot'; /** A root from `createRoot()` of "react-dom/client". */ type RenderWebChatRoot = { render(children: ReactNode): void }; +/** Containers `ReactDOM.render()` accepts, `null` included as in its typings (it throws at run time). */ +type RenderWebChatContainer = Element | Document | DocumentFragment | null; + type RenderWebChat = { /** * Renders Web Chat into an element, or with a root from `createRoot()`. @@ -14,7 +17,7 @@ type RenderWebChat = { * React 19 or later removed `ReactDOM.render()`: pass `createRoot(element)` instead of the element. * @throws When an element is passed on React 19 or later, or when `elementOrRoot` is neither an element nor a root. */ - (props: any, elementOrRoot: HTMLElement | RenderWebChatRoot): void; + (props: any, elementOrRoot: RenderWebChatContainer | RenderWebChatRoot): void; }; // Same as `Node.ELEMENT_NODE`, `Node.DOCUMENT_NODE` and `Node.DOCUMENT_FRAGMENT_NODE`, without needing `Node` on import. @@ -24,7 +27,7 @@ const DOCUMENT_FRAGMENT_NODE = 11; // Containers `ReactDOM.render()` accepts: element, document and document fragment, by `nodeType` to work across realms. // Checked before `renderSchema`: some custom elements (e.g. Lit) have their own `render()` method. -const containerSchema = custom(value => +const containerSchema = custom>(value => is(object({ nodeType: picklist([DOCUMENT_FRAGMENT_NODE, DOCUMENT_NODE, ELEMENT_NODE]) }), value) ); @@ -34,7 +37,7 @@ const renderSchema = object({ render: function_() }); export default function renderWebChat( ReactWebChat: ComponentType, props: any, - elementOrRoot: HTMLElement | RenderWebChatRoot + elementOrRoot: RenderWebChatContainer | RenderWebChatRoot ): void { const children = ; @@ -56,4 +59,4 @@ export default function renderWebChat( } } -export { type RenderWebChat, type RenderWebChatRoot }; +export { type RenderWebChat, type RenderWebChatContainer, type RenderWebChatRoot };