Skip to content

fix(Lit.React): render with createRoot on React 18 and 19 - #83

Open
OnurGumus wants to merge 2 commits into
fable-hub:mainfrom
OnurGumus:fix/react-create-root
Open

OnurGumus wants to merge 2 commits into
fable-hub:mainfrom
OnurGumus:fix/react-create-root

Conversation

@OnurGumus

@OnurGumus OnurGumus commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

React.toLit renders with ReactDom.render, which React 18 deprecated and React 19 removed. On React 19 the directive raises and renders nothing:

TypeError: react_dom.render is not a function
    at Lit.React.js:46:31
    at RefDirective._updateRefValue (lit/directives/ref.js)

This switches it to createRoot from react-dom/client.

What changes

  • src/Lit.React/Lit.React.fs: the directive keeps one root per element. A new root is created only when lit hands the directive a different element, and the old one is unmounted then. The root is also unmounted when the directive disconnects. Fable.React 9.4.0 binds only the old react-dom entry point, so the two members of react-dom/client this needs (createRoot, and a root's render/unmount) are declared in the file.
  • package.json: react and react-dom go to ^19, because the sample renders a Feliz component through this directive. The docs toolchain (nacara, gatsby 3) declares react 16/17 as a peer, so npm refuses the install outright. An overrides entry points react and react-dom at the repo's own versions. The lock file changes only for that (under 100 lines, written with npm 10 as CI uses).
  • test/react-check/: a manual check page, run with npm run check:react. npm test can't load React: it ships CommonJS and reads process.env at import time, and the test runner serves ES modules to a browser that has neither. The page renders a component through the directive, renders it again with new props, and leaves the result on window.__result.

Requires React 18 or newer. Apps still on React 17 should stay on the current Fable.Lit.React.

How I checked it

  • dotnet build Fable.Lit.sln and npm test (37 passed), the same steps as test.yml.
  • npm run check:react on React 19.3.1: the page renders "one", then "two" after new props, with no errors. With the old Lit.React.fs the same page throws the TypeError above.
  • The sample (sample/App.fs) on React 19.3.1: the React card renders, its useState counts clicks, hiding it runs its effect cleanup, and showing it again mounts a fresh one.

Not covered

  • Docs build. npm run docs doesn't work on main today on current Node: on Node 25 nacara passes an option to rmdir that Node removed, and on Node 22 it fails looking for build/docs. With this change it also fails on import "react-dom/server.js", which React 19 doesn't export. Nothing in CI builds the docs, so this PR doesn't break a working pipeline. Whoever revives the docs will still need to replace or patch nacara for React 19.
  • React state across lit re-renders. lit hands the directive a new wrapper element on each re-render of the surrounding template, so the React component is mounted afresh and loses its state. That was equally true with ReactDom.render, and fixing it is a bigger change than this.

🤖 Generated with Claude Code

OnurGumus and others added 2 commits October 19, 2026 08:41
The directive rendered with ReactDom.render, deprecated in React 18 and
removed in 19, so on a current React it raised "react_dom.render is not a
function" and rendered nothing. It now creates a root through
react-dom/client and renders into that.

The root is kept per element: one root for the element lit is rendering
into, unmounted when lit hands the directive a different element and when
the directive disconnects. Fable.React binds only the old react-dom entry
point, so the two members of react-dom/client this needs are declared in
the file.

react and react-dom move to 19 in the repo, since the sample renders a
React component through this directive. The docs toolchain (nacara,
gatsby 3) pins react 16/17 as a peer, so package.json overrides react and
react-dom to the repo's own versions rather than letting npm refuse.

npm test cannot load React (it ships CommonJS and reads process.env at
import time), so test/react-check is a page that renders a component
through the directive, renders it again with new props and leaves the
result on window.__result. Run it with npm run check:react.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npm 11 prunes optional peers that npm 10's `npm ci` still expects, so the
lock from the previous commit failed CI with "Missing: type-fest@0.13.1".
Regenerated with npm 10.9.9 on Node 22, which is what test.yml runs; npm 11
accepts it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JordanMarr JordanMarr added the after-2.0 Considered after the 2.0 maintenance release (see #90) label Oct 19, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

after-2.0 Considered after the 2.0 maintenance release (see #90)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants