Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: Loading components in an included Turbo Frame #1113

Open
multiplegeorges opened this issue Apr 27, 2021 · 3 comments
Open

PoC: Loading components in an included Turbo Frame #1113

multiplegeorges opened this issue Apr 27, 2021 · 3 comments

Comments

@multiplegeorges
Copy link

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!

@phoozle
Copy link

phoozle commented Jul 6, 2021

Thanks very much!

@buncis
Copy link
Contributor

buncis commented Nov 23, 2021

is this a solution when the react component not attached/loaded in the page that reloaded using turbo?

@multiplegeorges
Copy link
Author

@buncis Yes, this code should help when you need to initialize a React component that is lazy loaded in a Turbo Frame after the initial page load.

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

No branches or pull requests

3 participants