10

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>
  );
}
GWorking
  • 4,011
  • 10
  • 49
  • 90
  • 3
    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 content – alistair Apr 24 '20 at 12:18
  • Can you explain what is the problem you trying to solve? You want to update the `textContent`? – Dennis Vash Apr 24 '20 at 12:26
  • @aabbccsmith that useEffect is called when `plainhtml` is available (fetched from an external server), but the code example doesn't need to include this – GWorking Apr 24 '20 at 12:58
  • @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) – GWorking Apr 24 '20 at 13:01
  • @GWorking I've updated the answer, check it out. – Dennis Vash Apr 24 '20 at 13:03

2 Answers2

6

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

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
  • 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 `dangerouslySetInnerHTML` – GWorking Apr 24 '20 at 12:57
  • 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. – Dennis Vash Apr 24 '20 at 12:58
  • 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) – GWorking Apr 24 '20 at 13:02
  • Thanks again (for your time :)) but the quid is to insert this ref inside the `htmlString` – GWorking Apr 24 '20 at 13:07
  • So just use append child and so on. You **got** the reference which is actually what you asked. – Dennis Vash Apr 24 '20 at 13:08
  • 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?) – GWorking Apr 24 '20 at 13:12
  • Do you just want the reference of the HTML that `dangerouslySetInnerHTML` renders? Like `
    Hello
    ` in my example, you want this `myRef`?
    – Dennis Vash Apr 24 '20 at 13:14
  • 1
    (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 there – GWorking Apr 24 '20 at 13:23
3

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>
  );
}
GWorking
  • 4,011
  • 10
  • 49
  • 90