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

2

14

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>
  );
}
Camelback answered 24/4, 2020 at 12:15 Comment(5)
Why are you calling ReactDOM.render inside a useEffect? You should just render it as a child, that way you would be able to use state to set the text contentSeminal
Can you explain what is the problem you trying to solve? You want to update the textContent?Anesthetize
@aabbccsmith that useEffect is called when plainhtml is available (fetched from an external server), but the code example doesn't need to include thisCamelback
@DennisVash I receive a html string from an external server which I can show with dangerouslySetInnerHTML, but then I want to insert there some React components, the way of doing this is through .render(), but then when I want to use the refs they're not available (they are if I wait few milliseconds). So the question is how to have access to them (which is what the code example shows)Camelback
@Camelback I've updated the answer, check it out.Anesthetize
C
5

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>
  );
}
Camelback answered 24/4, 2020 at 13:54 Comment(0)
A
9

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

Anesthetize answered 24/4, 2020 at 12:33 Comment(8)
Thanks for the answer, but the question is specific about getting the ref available when inserted within a plain html code that is itself inserted with dangerouslySetInnerHTMLCamelback
You need the component to mount in order to have the ref, I'll add an example of what I think you trying to achieve.Anesthetize
This is happening once I receive the plain html content, that I want to inject, but that I want to contain some components to change them. I can do it with vanilla document.getElementById, but trying to do it the React way (i.e. useRef)Camelback
Thanks again (for your time :)) but the quid is to insert this ref inside the htmlStringCamelback
So just use append child and so on. You got the reference which is actually what you asked.Anesthetize
I'm afraid I am not explaining myself too well, the reference that you provide is not inside the htmlString, is a regular reference with no relation to htmlString. I ask for the former, I am not asking about how to get a reference in React (is that what is understood in the question?)Camelback
Do you just want the reference of the HTML that dangerouslySetInnerHTML renders? Like <div id="container" ref={myRef}>Hello</div> in my example, you want this myRef?Anesthetize
(updated the question to better show what I want) What I'd want is to modify some elements that are inside the HTML that dangerouslySetInnerHTML renders, not the whole HTML but some elements thereCamelback
C
5

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>
  );
}
Camelback answered 24/4, 2020 at 13:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.