Javascript dynamically inserted later on: how to make it run?

1.7k views Asked by At

I have scripts In my React app that are inserted dynamically later on. The scripts don't load.

In my database there is a field called content, which contains data that includes html and javascript. There are many records and each record can include multiple scripts in the content field. So it's not really an option to statically specify each of the script-urls in my React app. The field for a record could for example look like:

<p>Some text and html</p>
<div id="xxx_hype_container">
    <script type="text/javascript" charset="utf-8" src="https://example.com/uploads/hype_generated_script.js?499892"></script>
</div>
<div style="display: none;" aria-hidden="true"> 
<div>Some text.</div> 
Etc…

I call on this field in my React app using dangerouslySetInnerHTML:

render() {
    return (
        <div data-page="clarifies">
            <div className="container">
                <div dangerouslySetInnerHTML={{ __html: post.content }} />
                ... some other data
            </div>
        </div>
    );
}

It correctly loads the data from the database and displays the html from that data. However, the Javascript does not get executed. I think the script doesn't work because it is dynamically inserted later on. How can I make these scripts work/run?

This post suggest a solution for dynamically inserted scripts, but I don't think I can apply this solution because in my case the script/code is inserted from a database (so how to then use nodeScriptReplace on the code...?). Any suggestions how I might make my scripts work?


Update in response to @lissettdm their answer:

constructor(props) {
    this.ref = React.createRef();
}

componentDidUpdate(prevProps, prevState) {
    if (prevProps.postData !== this.props.postData) {
        this.setState({
            loading: false,
            post: this.props.postData.data,
            //etc
        });
        setTimeout(() => parseElements());

        console.log(this.props.postData.data.content);
        // returns html string like: `<div id="hype_container" style="margin: auto; etc.`
        const node = document.createRange().createContextualFragment(this.props.postData.data.content);
        console.log(JSON.stringify(this.ref));
        // returns {"current":null}
        console.log(node);
        // returns [object DocumentFragment]
        this.ref.current.appendChild(node);
        // produces error "Cannot read properties of null"
    }
}

render() {
    const { history } = this.props;
    /etc.
    return (
        {loading ? (
            some code
        ) : (
            <div data-page="clarifies">
                <div className="container">
                    <div ref={this.ref}></div>
                    ... some other data
                </div>
            </div>
        );
    );
}

The this.ref.current.appendChild(node); line produces the error:

TypeError: Cannot read properties of null (reading 'appendChild')

4

There are 4 answers

10
lissettdm On BEST ANSWER

If your are sure about HTML string content is safety and contains a string with valid HTML you can use Range.createContextualFragment() (executes scripts )

function App() {
  const ref = useRef();

  useEffect(() => {
    /* convert your HTML string into DocumentFragment*/
    const node = document.createRange().createContextualFragment(HTML);
    ref.current.appendChild(node);
  }, []);

  return (
    <div>
      <h1>HTML String</h1>
      <div>
        <div ref={ref}></div>
      </div>
    </div>
  );
}

See how script content is executed on JavaScript console working example

If your are using class component create ref within class constructor, then update node content, I did it in componentDidMount just for testing:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.ref = React.createRef();
  }

  componentDidMount() {
    const node = document.createRange().createContextualFragment(HTML);
    this.ref.current.appendChild(node);
  }

  render() {
    return (
      <div>
        <h1>HTML String</h1>
        <div>
          <div ref={this.ref}></div>
        </div>
      </div>
    );
  }
}

see this working example

0
innocent On

You would need to first fetch the dynamic data from the db using a fetch call and make use of useEffect, Inside which after the data is fetched you set it to a useState hook variable which will hold the data for this.

const [dbData,setDbData] = useState([]);

useEffect(()=>{
  const dbReqFunc = async () => {
    // req your dynamic data here and set the data fetched to dbData
    // using setDbData(...)
  };

  dbReqFunc();
},[]);  // Making the call on component loading (Modify accordingly based on 
        // needs)

After this once the data has been set you can make use of another useEffect hook which should be below the previous useEffect hook. Then you can call your function for the entire dynamic URL fetched and append it to the HTML document ( I have attached all the scripts to a div for easy cleanup).

useEffect(()=>{
  if(dbData.length>0){
    const elem = document.createElement('div');
    dbData.map(data=> {

        // based on data returned modify accordingly

        const script = document.createElement("script");
        script.src = '...script-url...';
        script.async = true;
        script.onload = function() {
          // Do something
        };
     
        //...other script fields...       

        elem.appendChild(script);
      });
     
     // attach elem to body
     document.body.appendChild(elem);

     return ()=>{
      // clean-up function
        document.body.removeChild(elem);
     };
  }

},[dbData]);

This should load the script data and should load the scripts.

NOTE: Make sure you are putting the the dynamic db call for fetching the data before the 2nd useEffect. So, that it runs in order.

1
Everyways On

Rendering raw HTML without React recommended method is not a good practice. React recommends method dangerouslySetInnerHTML to render raw HTML.

1
Amit On

There are various ways to do this. You may create a function that can be called on to dynamically create and inject the <script> tag into the <body> of the React application.

const addScript = () => {
    const script = document.createElement("script");
    script.src = '<url-of-the-script>';
    script.async = true;
    script.onload = function() {
        // Do something
    };
    document.head.appendChild(script);
}

You may call this addScript function when the required component loads using the useEffect hook.

useEffect(() => {
    addScript();
    return () => {
        // remove the script on component unmount
    };
}, []);