Skip to content

Render with createRoot() in renderWebChat() when ReactDOM.render() is not available - #5963

Merged
Eugene (OEvgeny) merged 8 commits into
mainfrom
OEvgeny/fix-render-webchat-without-render
Oct 23, 2026
Merged

Eugene (OEvgeny) merged 8 commits into
mainfrom
OEvgeny/fix-render-webchat-without-render

Conversation

@OEvgeny

@OEvgeny Eugene (OEvgeny) commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Related to #5552 and #5850.

Changelog Entry

Added

  • renderWebChat(): added support of a root from createRoot() for React 19, related to #5552 and #5850, in PR #5963, by @OEvgeny

Description

renderWebChat() called ReactDOM.render(), which React 19 removed. It now also takes a root from createRoot() instead of the element; an element on React 19 throws, asking for createRoot(element).

Design

  • Web Chat never imports react-dom/client (npm bundlers fail on it with React 16 and 17): the host creates the root.
  • DOM containers are checked first (Lit elements have render()); React 16 to 18 keep ReactDOM.render(), synchronously.

Specific Changes

  • renderWebChat.tsx: element or root, typed; renderWebChat.spec.tsx: element, fragment, root, React 19 error
  • isomorphic-react-dom: bundled React DOM loads only without window.ReactDOM (crashed with React 19)
  • react19/renderWebChat.{iife,esm}.html: element throws; snapshot 1 root, 2 same root shows the send box
  • react19/renderWebChat.{root,customElement}.react18.esm.html: root; custom element with render(); snapshot 1
  • HOSTING.md: React 19 passes createRoot(element)
  • I have added tests and executed them locally
  • I have added .changelog/<PR#>-<slug>.md
  • I have updated documentation

Review Checklist

This section is for contributors to review your work.

  • Accessibility reviewed (tab order, content readability, alt text, color contrast)
  • Browser and platform compatibilities reviewed
  • CSS styles reviewed (minimal rules, no z-index)
  • Documents reviewed (docs, samples, live demo)
  • Internationalization reviewed (strings, unit formatting)
  • package.json and package-lock.json reviewed
  • Security reviewed (no data URIs, check for nonce leak)
  • Tests reviewed (coverage, legitimacy)

@OEvgeny
Eugene (OEvgeny) marked this pull request as ready for review October 22, 2026 20:53
Copilot AI balanced review requested due to automatic review settings October 22, 2026 20:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.


// WHEN: Web Chat is rendered with createRoot(), as the error says.
createRoot(webChatElement).render(
createElement(ReactWebChat, { directLine, store, styleOptions: { hideSendBox: true } })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we test it like *.esm.html, where it set the style options with { hideSendBox: false } on second render?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in d57b101: after the first snapshot, the IIFE test renders <ReactWebChat> again on the same root with styleOptions: { hideSendBox: false }. The send box appears, the transcript stays the same element, and snapshot 2 shows the message with the send box, so the IIFE bundle handles a prop update on the page's React 19. renderWebChat() itself fails on the IIFE path before creating a root, so root reuse stays covered by the esm test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 0a88658: the IIFE test (React 19) now calls renderWebChat() itself with the page's root, then again on the same root with styleOptions: { hideSendBox: false }. The send box appears, the transcript stays the same element, snapshot 2 is unchanged, and root.unmount() removes Web Chat.

Comment thread packages/bundle/src/renderWebChat.tsx Outdated
Comment thread packages/bundle/src/renderWebChat.tsx Outdated
importReactDOMClient() always returns a promise; renderWebChat() returns
Promise<void> 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.
Comment thread packages/bundle/src/renderWebChat.tsx Outdated
Eugene Olonov added 5 commits October 23, 2026 11:23
…er import react-dom/client

Ported from 92bbd36 and d44517f. 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.
… React 18, and a custom element with render()

Ported from 2fb2d04: 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 }.
…cument | DocumentFragment | null) or a root
Eugene (OEvgeny) pushed a commit that referenced this pull request Oct 23, 2026
… OEvgeny/dev-react-19-on-film-stack

#5963 now takes renderWebChat(props, elementOrRoot), synchronous, and never imports "react-dom/client" (importReactDOMClient and its esbuild plugin removed).

Conflicts resolved to #5963: packages/bundle/src/renderWebChat.tsx, renderWebChat.spec.tsx, and the react19/renderWebChat.{iife,root.react18.esm,customElement.react18.esm}.html tests (stage 4's own root tests are superseded by #5963's).

Kept from this branch: React 19 tests, the Fluent IIFE React DOM fix, HOSTING.md React 19 preview section (updated: hosts pass createRoot(element); no "react-dom/client" in the import map for renderWebChat; npm bullet removed). react19/esm.renderWebChat.html now passes createRoot(element).
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for",
"botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js",
"react": "/__dist__/packages/bundle/static/react.js",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use esm.sh/react@19.3.1 as well?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried: the static build fails with "'react-dom' does not provide an export named 'render'", from react-film's import { render } from 'react-dom'. Real React 19 with the static build is tested on the react-film stack (react19/esm.renderWebChat.html); this page switches once react-film is gone (TODO updated in #5971).

const webChatElement = document.getElementById('webchat');

// GIVEN: "react-dom" without `ReactDOM.render()`, like React 19.
expect(ReactDOM.render).toBeUndefined();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add expect(ReactDOM.version).toBe(expect.stringMatching(/^19\./)) for clearer expectation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #5971 (69ed9a9ed). This page runs React 18 with ReactDOM.render() removed (the static build cannot load real React 19 yet), so it expects /^18\./; the real React 19 pages (IIFE, Fluent IIFE, copy button) expect /^19\./. toEqual(), as toBe() ignores asymmetric matchers.

// GIVEN: A root from createRoot(), which records what it renders.
const root = createRoot(document.getElementById('webchat'));
const renderedChildren = [];
const recordingRoot = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider use jest-mock/spyOn.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #5971 (c1693e72d): spyOn(root, 'render') from jest-mock replaces the hand-written recording root; it still calls through.

// 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().');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, to clearer on where createRoot() is from.

Suggested change
throw new Error('botframework-webchat: renderWebChat() expects an HTMLElement or a root from createRoot().');
throw new Error('botframework-webchat: renderWebChat() expects an HTMLElement or a root from ReactDOM.createRoot().');

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #5971 (46e59d55c): applied as suggested; the unit spec and the IIFE test expect the new message.

);

// Both `ReactDOM.render()` (React 18 or earlier) and `root.render()`.
const renderSchema = object({ render: function_() });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: maybe split them into 2 schemas for clearer code paths below.

  • reactRootSchema: object({ render: pipe(function_(), args(...)) })
  • legacyReactDOMSchema: object({ render: pipe(function_(), args(...)) })

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#5963 merged just before I pushed this. The split is ready (legacyReactDOMSchema checked on ReactDOM, reactRootSchema on the root, same behaviour) and goes into the React 19 follow-up PR together with the other nits.

@OEvgeny
Eugene (OEvgeny) merged commit 60c356a into main Oct 23, 2026
33 checks passed
@OEvgeny
Eugene (OEvgeny) deleted the OEvgeny/fix-render-webchat-without-render branch October 23, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants