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..c54e7a0e76
--- /dev/null
+++ b/.changelog/5963-fix-render-webchat-without-render.md
@@ -0,0 +1,3 @@
+### Added
+
+- `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/__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.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 0000000000..041fcd2500
Binary files /dev/null and b/__tests__/html2/react19/renderWebChat.customElement.react18.esm.html.snap-1.png differ
diff --git a/__tests__/html2/react19/renderWebChat.esm.html b/__tests__/html2/react19/renderWebChat.esm.html
new file mode 100644
index 0000000000..ac79ff7a4d
--- /dev/null
+++ b/__tests__/html2/react19/renderWebChat.esm.html
@@ -0,0 +1,80 @@
+
+
+
+ renderWebChat() renders with the root passed 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 0000000000..041fcd2500
Binary files /dev/null and b/__tests__/html2/react19/renderWebChat.esm.html.snap-1.png differ
diff --git a/__tests__/html2/react19/renderWebChat.esm.html.snap-2.png b/__tests__/html2/react19/renderWebChat.esm.html.snap-2.png
new file mode 100644
index 0000000000..575a402053
Binary files /dev/null and b/__tests__/html2/react19/renderWebChat.esm.html.snap-2.png differ
diff --git a/__tests__/html2/react19/renderWebChat.iife.html b/__tests__/html2/react19/renderWebChat.iife.html
new file mode 100644
index 0000000000..221d62ab38
--- /dev/null
+++ b/__tests__/html2/react19/renderWebChat.iife.html
@@ -0,0 +1,97 @@
+
+
+
+ React 19: renderWebChat() of the IIFE build renders with the root passed
+
+
+
+
+
+
+
+
+
+
+
+
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 0000000000..041fcd2500
Binary files /dev/null and b/__tests__/html2/react19/renderWebChat.iife.html.snap-1.png differ
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 0000000000..575a402053
Binary files /dev/null and b/__tests__/html2/react19/renderWebChat.iife.html.snap-2.png differ
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 0000000000..041fcd2500
Binary files /dev/null and b/__tests__/html2/react19/renderWebChat.root.react18.esm.html.snap-1.png differ
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
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/renderWebChat.spec.tsx b/packages/bundle/src/renderWebChat.spec.tsx
new file mode 100644
index 0000000000..1ca156f321
--- /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);
+
+ 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 a702bdd147..d1e9230573 100644
--- a/packages/bundle/src/renderWebChat.tsx
+++ b/packages/bundle/src/renderWebChat.tsx
@@ -1,6 +1,62 @@
-import React, { ComponentType } from 'react';
+import React, { type ComponentType, type ReactNode } from 'react';
import ReactDOM from 'react-dom';
+import { custom, function_, is, object, picklist } from 'valibot';
-export default function renderWebChat(ReactWebChat: ComponentType, props: any, element: HTMLElement): void {
- ReactDOM.render(, element);
+/** 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()`.
+ *
+ * @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: RenderWebChatContainer | 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)
+);
+
+// Both `ReactDOM.render()` (React 18 or earlier) and `root.render()`.
+const renderSchema = object({ render: function_() });
+
+export default function renderWebChat(
+ ReactWebChat: ComponentType,
+ props: any,
+ elementOrRoot: RenderWebChatContainer | RenderWebChatRoot
+): void {
+ const children = ;
+
+ 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 {
+ throw new Error('botframework-webchat: renderWebChat() expects an HTMLElement or a root from createRoot().');
+ }
}
+
+export { type RenderWebChat, type RenderWebChatContainer, type RenderWebChatRoot };
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;