Search code examples
javascriptreactjsreact-hooksuse-effectuse-ref

React: How to wait until ref is available when inserted (rendered) within a second container


EDIT: better explanation

The context:

I receive some plain HTML code from a 3rd server, which I want to

  • insert in my React app
  • modify it

The vanilla JS approach

  • I can modify the string with regex and add any HTML tag with an id
  • Then I can modify these elements through getElementById, as usual

The React approach

  • I shouldn't use the DOM
  • Then I should insert within the string some components that have a React ref inside
  • The opposite (to insert some React components as plain HTML) would be through ReactDOMServer.renderToString
  • So, when I inject the components with ReactDOM.render(), the problem is that the render method takes its time, so that if in the next line I try to use the ref that exists in the inserted component, is not yet there

The question

  • How to do it? Usually I would put the code within a useEffect with a [] dependencies, but here I am rendering the component when the app is already mounted
  • A quick workaround is to just do an async wait of 500 ms, and then I can access the ref, but for sure there has to be something better

This code fails, because when the ref is rendered it is still not available, so ref.current is undefined

How can I wait for it?

codesandbox

EDIT: I provide the code that works but through direct DOM, which I assume should be avoided

import React, { useRef, useEffect } from "react";
import ReactDOM from "react-dom";

export default function App() {
  const myref = useRef();

  useEffect(() => {
    const Com = () => <div ref={myref}>hello</div>;
    ReactDOM.render(<Com />, document.getElementById("container"));
    console.log(myref.current); // undefined
    document.getElementById('container').textContent = "direct DOM works"

   // the next line fails since the ref is not yet available
   // myref.current.textContent = "but this REF is not available"; // fails
  }, []);

  const plainhtml = '<div><div id="container"></div><div>some more content</div><div id="another">even more content</div></div>'; // this is some large HTML fetched from an external server

  return (
    <div>
      <h1>Hello CodeSandbox</h1>
      <div dangerouslySetInnerHTML={{ __html: plainhtml }} />
    </div>
  );
}

Solution

  • I need to use a callback ref but encapsulating it within useCallback to make sure it only rerenders with the dependencies indicated (i.e. none []), so that it is only executed when the component changes (as explained here)

    codesandbox

    import React, { useEffect, useCallback } from "react";
    import ReactDOM from "react-dom";
    
    export default function App() {
      const measuredRef = useCallback(node => {
        if (node !== null) {
          node.textContent = "useCallback DOM also works";
        }
      }, []);
    
      useEffect(() => {
        const Com = () => <div ref={measuredRef}>hello</div>;
        ReactDOM.render(<Com />, document.getElementById("container"));
        document.getElementById("container").textContent = "direct DOM works";
      }, []);
    
      const plainhtml = '<div id="container"></div>';
    
      return (
        <div>
          <h1>Hello CodeSandbox</h1>
          <div dangerouslySetInnerHTML={{ __html: plainhtml }} />
        </div>
      );
    }