-
Notifications
You must be signed in to change notification settings - Fork 752
Description
Hey all,
Like many others, I've been experimenting with Turbo and Turbo Frames. We have a large set of one-off React components we like to sprinkle into our templates using react-rails and that pattern works great for enhancing interactivity.
But, like many others, we quickly realized that Turbo doesn't emit any events when a frame loads. This is by design and their logic makes sense.
To fix this we've put together a quick proof of concept MutationObserver that watches the document tree for changes. These changes could come from any source, but in our case it's always a Turbo Frame load. I don't think there any need to differentiate based on the source of the change.
import React from 'react'
import ReactDOM from 'react-dom'
declare const ReactRailsUJS
document.addEventListener("DOMContentLoaded", () => {
const findComponents = (childNodes: NodeList, testFn: (n: Node) => Boolean, nodes: Node[] = []): Node[] => {
for (let child of childNodes) {
if (child.childNodes.length > 0) {
nodes = findComponents(child.childNodes, testFn, nodes)
} else if (testFn(child)) {
nodes = nodes.concat([child])
}
}
return nodes
}
const mountComponents = (nodes: Node[]) => {
for (let child of nodes) {
const className = (child as Element).getAttribute(ReactRailsUJS.CLASS_NAME_ATTR)
if (className) {
// Taken from ReastRailsUJS as is.
const constructor = ReactRailsUJS.getConstructor(className)
const propsJson = (child as Element).getAttribute(ReactRailsUJS.PROPS_ATTR)
const props = propsJson && JSON.parse(propsJson)
// Improvement:
// Was this component already rendered? Just hydrate it with the props coming in.
// This is currently acceptable since all our components are expected to be reset
// on page navigation.
const component = React.createElement(constructor, props) as any
ReactDOM.render(component, child as Element)
}
}
}
const callback = function (mutationsList: MutationRecord[], observer: MutationObserver) {
const start = performance.now()
console.log("ReactRails: Mutation callback started...", mutationsList)
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length > 0) {
const mountableNodes = findComponents(mutation.addedNodes, (child) => {
return !!(child as HTMLElement).dataset?.reactClass
})
mountComponents(mountableNodes)
}
}
}
console.log("ReactRails: Mutation callback complete.", performance.now() - start)
};
const observer = new MutationObserver(callback)
console.log("ReactRails: Start mutation observer...")
observer.observe(document, { childList: true, subtree: true })
})
We've simply added this to our application.js pack file and we've found that this works quite well.
Hopefully this helps someone else out and/or starts a discussion about moving react-rails to this model for mounting/unmounting components. It's a lot more robust than watching for Turbo events, I think, but I'm sure there's a ton of edge cases covered by the existing code.
Cheers!