Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/chubby-towns-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/react-devtools': minor
'@tanstack/devtools': minor
---

Renders the devtools in a shadow DOM node
12 changes: 12 additions & 0 deletions examples/react/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@
<description
>A basic example of using TanStack Devtools with React.</description
>

<!--
These styles exist only to verify that the Devtools UI
does NOT inherit global styles. Because the devtools are
rendered inside a Shadow Root, the rules below should have
no visible effect.
-->
<style type="text/css">
[data-testid='tanstack_devtools'] * {
color: red !important;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
23 changes: 22 additions & 1 deletion packages/devtools/src/devtools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,30 @@ export default function DevTools() {
})
const { theme } = useTheme()

const [gooberCss, setGooberCss] = createSignal('')
createEffect(() => {
// Setup mutation observer for goober styles with id `_goober
const gooberStyles = document.querySelector('#_goober')
if (gooberStyles) {
setGooberCss(gooberStyles.textContent)
const observer = new MutationObserver(() => {
setGooberCss(gooberStyles.textContent)
})
observer.observe(gooberStyles, {
childList: true, // observe direct children
subtree: true, // and lower descendants too
characterDataOldValue: true, // pass old data to callback
})
onCleanup(() => {
observer.disconnect()
})
}
})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

So "syncing" the styles from the parent page to the shadow DOM node is not what I initially set out to do. In fact, there were two approaches I made prior to this, but both failed.

  • Approach 1: Leverage goobers css.bind({ target: <element> }) feature.

    • This basically involves creating a context provider that binds the CSS target and passes that down to child components to use.
    • This looked like a useGoober hook in the UI library, that provides the CSS bound target for the shadow node
    • Just did not work. Even when binding the target manually, goober always appended the styles to the head of the owner document and not the shadow root. Both UI and devtool packages were implementing the context packages correctly.
    • Reference material, Goober Docs
    • Perhaps this is worth a revisit
  • Approach 2: Just do Goobers extractCSS

    • Only works on the server 🤷


return (
<ThemeContextProvider theme={theme()}>
<Portal mount={(pip().pipWindow ?? window).document.body}>
<Portal mount={(pip().pipWindow ?? window).document.body} useShadow>
<style>{gooberCss()}</style>
<div ref={setRootEl} data-testid={TANSTACK_DEVTOOLS}>
<Show
when={
Expand Down
13 changes: 4 additions & 9 deletions packages/react-devtools/src/devtools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,10 @@ export const TanStackDevtools = ({
},
render: (e, theme) => {
const id = e.getAttribute('id')!
const target = e.ownerDocument.getElementById(id)

if (target) {
setPluginContainers((prev) => ({
...prev,
[id]: e,
}))
}

setPluginContainers((prev) => ({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we are rendering the component in a shadow DOM node, e.ownerDocument.getElementById(id) always returns null and the plugins are never mounted.

I'm not certain I understand what this if statement guards against since I thought the elements were defined by the devtools plugin itself. Shouldn't they always exist?

...prev,
[id]: e,
}))
convertRender(plugin.render, setPluginComponents, e, theme)
},
}
Expand Down
Loading