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

All we need is an easy explanation of the problem, so here it is.

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>
  );
}

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

useEffect with empty dependency array executes after the first render, therefore you will get the DOM ref in the callback:

const htmlString = '<div id="container">Hello</div>';

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

  useEffect(() => {
    if (myRef.current) {
      myRef.current.textContent = 'whats up';
    }
    console.log(myRef.current);
  }, []);

  return (
    <div>
      <div ref={myRef} dangerouslySetInnerHTML={{ __html: htmlString }} />
      <div dangerouslySetInnerHTML={{ __html: htmlString }} />
    </div>
  );
}

/* App renders:
whats up
Hello
*/

Edit nervous-glade-8rtxb

Method 2

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>
  );
}

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply