nfl / react-helmet

A document head manager for React
MIT License
17.36k stars 661 forks source link

Scripts are not loaded synchronously within the same Helmet component #419

Open darkowic opened 5 years ago

darkowic commented 5 years ago

I would like to inject 2 script tags like this: jQuery is just an example

      <Helmet>
        <script
          src="https://code.jquery.com/jquery-3.3.1.min.js"
          integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
          crossorigin="anonymous"
        ></script>
        <script>
          {`
            console.log('Test', typeof $);
          `}
        </script>
      </Helmet>

After the first script is loaded I want to execute the second inline script. Normally, when you put 2 scripts into HTML without any async or defer they will load synchronously.

Currently both scripts will be executed asynchronously. See reproduction here - https://codesandbox.io/s/l9qmrwxqzq

tmbtech commented 5 years ago

Unfortunately this is a known issue. We need to do a better job at communicating this bug to the everyone.

Note to self add a FAQ to the readme related to this bug.

darkowic commented 5 years ago

@tmbtech definitely :) I was looking for it through the issues and did not found it

tmbtech commented 5 years ago

In the meantime, internally we do something similar to this , maybe it'll help.


class LoadExternalScript extends React.Component {
    static defaultProps = {
        onLoad: () => {},
        onError: () => {},
    };

    render() {
        const {props} = this;
        if (!canUseDOM) {
            props.onError("DOM not found");
            return false;
        }

        if (!document.getElementById(props.id)) {
            const script = document.createElement("script");
            script.src = props.src;
            script.id = props.id;
            script.onload = props.onLoad;
            script.onerror = props.onError;

            if (document.body) {
                document.body.appendChild(script);
            }
        } else {
            props.onError("script already loaded");
        }

        return false;
    }
}

// some other file

class Foobar extends React.Component {
  state = {
      status: "pending"
  } 
  render() {
     return (
         <LoadExternalScript 
              src="example.com" 
              id="example" 
              onLoad={() => this.setState({status: "scriptLoaded"})}
         />
     )
  }
}

<Foobar /> can then have additional checks on if needs to load a script again and what components it needs to render after the global js script has loaded.

or depending on your case, you might just want to load the script in the html template? idk just throwing out ideas.

goodluck.

jamelait commented 5 years ago

Any workaround?

Zenoo commented 5 years ago

Still having the same issue here. Is there any way to fix it?

Edit: Since I didn't find any workaround, I created one myself, with the package react-append-head

BigSully commented 5 years ago

Still have this issue. I have a bunch of <scripts> in <Helmet>, but they seems to not execute in the order they are declared. My solution is that I manually downloaded all scripts and concat them into one script in the order they are declared in the original page.

zub2 commented 4 years ago

It seems that the script elements created by helmet have the async attribute and AFAIK there is no way how to disable it (e.g. by passing async={false} in JSX - this just sets the value of async to "false" but for unsetting it, the attribute would have to be removed).

raffazizzi commented 4 years ago

@zub2 setting async={false} as you suggested just worked for me! I'm using Helmet 6.1.0 within Gatsby 2.23.3 with TypeScript

KudMath commented 3 years ago

@zub2 setting async={false} as you suggested just worked for me! I'm using Helmet 6.1.0 within Gatsby 2.23.3 with TypeScript

did you pass async={false} to every script or to your Helmet HOC ?

cseas commented 3 years ago

This bug seems pretty old. Any update on this? https://stackoverflow.com/questions/65319422/append-multiple-script-tags-to-head-that-execute-synchronously-in-react/

jakezien commented 3 years ago

Super annoying that this is still a thing and that it's not documented in any obvious way. Took me way too long to figure out what was going on with my scripts. The readme says "Helmet takes plain HTML tags and outputs plain HTML tags. It's dead simple, and React beginner friendly." But plain HTML script tags load synchronously, and Helmet changes this behavior on the sly; that's not simple or beginner friendly.

WangNingning1994 commented 3 years ago

Super annoying

yanickrochon commented 2 years ago

Just found this. The onLoad attribute is also not set properly.

const handleScriptLoaded = () => { console.log("**** LOADED"); };
<Helmet>
   <script src="https://domain.com/path/to/script.js" onLoad={handleScriptLoaded} type="text/javascript" />
</Helmet>

Creates the element:

<script src="http://app.thermoform.localhost/auth/js/api.js" onload="() => {
    console.log(&quot;*** LOADED&quot;);
  }" type="text/javascript" data-rh="true"></script>

This is completely unusable.

vergieet commented 1 year ago

just another workaround

i'm giving my script setTimeout with 50ms, and it's worked fine and still acceptable on my side

jcubic commented 8 months ago

There was no workaround so this is what I used:

function getScript(script: string) {
    return new Promise((resolve, reject) => {
        const $script = document.createElement('script');
        $script.onload = resolve;
        $script.onerror = reject;
        $script.src = script;
        document.head.appendChild($script);
    });
}

function useScripts(user_scripts: string[]) {
    useLayoutEffect(() => {
        const scripts = [...user_scripts];
        (function loop() {
            if (scripts.length) {
                const script = scripts.shift();
                getScript(script).then(loop);
            }
        })();
    }, []);
}
ivan-robert commented 7 months ago

You can add a listener in the Helmet and use react states to check if the first script is loaded before executing the second . To do so use the onChangeClientState Helmet prop