Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do I know the scripts are ready? #146

Open
gocreating opened this issue May 22, 2016 · 22 comments
Open

How do I know the scripts are ready? #146

gocreating opened this issue May 22, 2016 · 22 comments
Assignees

Comments

@gocreating
Copy link

I use Helmet to load scripts like firebase library. Is there an event handler that tells me when the scripts are loaded and ready? Now I can't access my library since I use it inside componentDidMount but the library is downloaded asynchronously after render Helmet

@potench
Copy link
Contributor

potench commented May 22, 2016

if you're server side rendering helmet then the scripts loaded in <head> are guaranteed to be loaded before react renders (async or not).
It sounds like you aren't server-side rendering helmet, so it might make more sense to use a script-loader in your case - but we'll look into adding an onload callback.

@romanonthego
Copy link
Contributor

script onload is sometimes unreliable.

We ended up just checking global (window.firebase or something) with setInterval until it became available and then pass it down a Promise to use.

Even if you use server-side rendering it may be useful since sometimes you only want to load script after some user action on client (for example - loading Facebook SDK only if it specifically requested). Save you precious kb's on initial load.

@DavidWells
Copy link

I am in need of an onLoad callback too =)

@romanonthego the native DOM scriptNode.addEventListener('load', func) was unreliable? Interesting.

What does your setInterval implementation look like?

Thanks

@navgarcha
Copy link

Is there any plans to add a onload option when specifying our scripts then? Think this would be really helpful.

@romanonthego
Copy link
Contributor

romanonthego commented Jan 10, 2017

export default function waitForGlobal(name, timeout = 300) {
  return new Promise((resolve, reject) => {
    let waited = 0

    function wait(interval) {
      setTimeout(() => {
        waited += interval
        // some logic to check if script is loaded
        // usually it something global in window object 
        if (window[name] !== undefined) {
          return resolve()
        }
        if (waited >= timeout * 1000) {
          return reject({ message: 'Timeout' })
        }
        wait(interval * 2)
      }, interval)
    }

    wait(30)
  })
}

@navgarcha
Copy link

navgarcha commented Jan 10, 2017

FYI we went with the following solution to hack in the onload functionality /cc @romanonthego :

handleScriptInject({ scriptTags }) {
    if (scriptTags) {
        const scriptTag = scriptTags[0];
        scriptTag.onload = this.handleOnLoad;
    }
}

// ...

<Helmet
    script={[{ src: '//cdn.example.com/script.js' }]}
    // Helmet doesn't support `onload` in script objects so we have to hack in our own
    onChangeClientState={(newState, addedTags) => this.handleScriptInject(addedTags)}
/>

@danielkcz
Copy link

@navgarcha Thanks, definitely much better approach than timers :) Works like a charm although I have rather used scriptTag.addEventListener('load') as it feels bit cleaner.

@navgarcha
Copy link

Good call - save myself from the 'no-param-reassign' eslint warning too ;)

@danielkcz
Copy link

danielkcz commented Jan 24, 2017

In the end I've just used scriptjs module instead. It's rather cumbersome to use helmet for this especially if there is more scripts being loaded. It's then necessary to iterate through script tags and find the one you are actually interested in ... and do that in every component. The scriptjs is definitely much easier and can be used as dependency manager too.

scriptjs('https://cdn.shopify.com/s/assets/external/app.js', () => {
	this.setState({ loaded: true })
})

It doesn't work on a server thou ... I guess you have to stick to helmet in that case.

@roastlechon
Copy link

I am running into this issue as well. Has anyone taken a stab at another solution?

I like the power of react-helmet for SSR, but would love it even more if we can wait for helmet to finish making updates to the DOM before rendering/executing/doing work.

Scenario is to async download and evaluate lodash library and use it in the component before mounting or updating.

Potentially have Helmet used with a higher order component to delay mounting until Helmet scripts are on the DOM.

@smileart
Copy link

smileart commented Mar 21, 2018

Somehow I was sure this functionality was already there. If it allows us to add script tags dynamically, sure thing it should assume we're going to use them, so we'd need some sort of a callback or any way to know that our scripts were loaded and are ready. Was surprised to find out that I was wrong. All the hacks suggested are sweet and so on, but I'd be happy to see this feature provided out of the box. Many others, I'm sure, would like it as well. Thanks anyway.

@JakobJingleheimer
Copy link

JakobJingleheimer commented Mar 27, 2018

Strangely, I could not get this to work—and it seems unnecessarily difficult to work around.

class Shell extends BaseComponent {
    // …

    handleResourceInjected({ resourcesByType }) {
        // each's might be slightly different (I don't remember the exact code)
        // the take-away is it looped over each resource-type group (styles, scripts, etc)
        // and then into each resource to check for an `onload` and call it if defined
        _.each(resourcesByType, (resourceType) => _.each(resourceType, ({ onload }) => {
            if (typeof onload === 'function') {
                debugger; // pauses
                onload(); // is indeed defined (shows bound function) &
                          // step-into jumps to the end of the callback,
                          // skipping debugger
            }
        }));
    }

    // …

    render() {
        return (
            // …
            <Helmet
                onChangeClientState={
                    (newState, resources) => this.handleResourceInjected(resources)
                }
            />
            // …
        );
    }
}

class SomeComponentWithUniqueDep extends BaseComponent {
    handleDepLoaded() {
        debugger; // never hit
        // do stuff
    }

    render() {
        return (<React.Fragment>
            <Helmet>
                <script
                    async
                    onLoad={() => this.handleDepLoaded()}
                    src="…"
                />
            </Helmet>
            // …
        </React.Fragment>);
    }
}

I also tried handleDepLoaded as a (non class method) function, and passing it to onLoad directly (both ways). No dice.

Note that "BaseComponent":

  • extends React.Component
  • dynamically binds this to all non-react-lifecycle methods

@mcabrams
Copy link

#299 This PR seems to address this issue, but has not been merged yet :(

@tmbtech tmbtech self-assigned this Nov 16, 2018
@fitfinderaustralia
Copy link

I ran into a similar need today and implemented something like this:
https://stackoverflow.com/questions/44877904/how-do-you-import-a-javascript-package-from-a-cdn-script-tag-in-react/53792272#53792272

Basically, make the external library reference a property of the state for the component. Make whatever component depends on that external library wait until this.state.libraryProperty is not null before attempting to use the external library. This avoids race conditions and null reference/undefined exceptions, although by my own admission there are probably ways to improve it - I don't like the fact that window is referenced for starters.

@PEM--
Copy link

PEM-- commented Jul 22, 2019

This does the trick for me:

const myScriptUrl = 'https:/blablabla/'

const MyComp = () => {
    const [scriptLoaded, setScriptLoaded] = useState(typeof window !== 'undefined' && typeof myScript !== 'undefined')
    const handleChangeClientState = (newState, addedTags) => {
        if (addedTags && addedTags.scriptTags) {
            const foundScript = addedTags.scriptTags.find(({ src }) => src === myScriptUrl)
            if (foundScript) {
                foundScript.addEventListener('load', () => setScriptLoaded(true), { once: true })
            }
        }
    }
    return <>
        <Helmet onChangeClientState={handleChangeClientState}>
            {typeof window !== 'undefined' && typeof myScript === 'undefined'
                && <script async defer src={stripeUrl} />}
        </Helmet>
        {scriptLoaded && ...}
    </>
}

This works pretty well with scripts like google analytics, stripe & co as they create an accessor in the global scope.

@suhanw
Copy link

suhanw commented Jan 29, 2020

Using onChangeClientState does not work if you use Helmet more than once. When you do, only the latest/most deeply nested implementation of onChangeClientState is executed. See this issue: #328

@fitfinderaustralia
Copy link

fitfinderaustralia commented Jan 29, 2020 via email

@sedatbasar
Copy link

sedatbasar commented Oct 13, 2020

I used the useScript hook on https://usehooks.com/useScript/ and worked pretty good as an alternative solution to helmet :)

@khitrenovich
Copy link

I used the useScript hook on https://usehooks.com/useScript/ and worked pretty good as an alternative solution to helmet :)

Note that useScript hook pushes scripts to body instead of head.
Some scripts (for example, Google Analytics) won't work correctly when loaded into document body.

@a-tonchev
Copy link

a-tonchev commented Nov 9, 2021

My solution is like this:

const onLoadFunction = () => { console.log('helmet loaded'); }

window.onHelmetLoad = onLoadFunction;

useEffect(() => {
  return () => {
    window.onHelmetLoad = null;
  }
}, []);


<Helmet>
        <title>Some Title</title>
        <meta property="og:image" content={ogImage} />
        <meta property="og:title" content={ogTitle} />
        <script>window.onHelmetLoad?.()</script>
</Helmet>

Notes:

  • window.onHelmetLoad is placed as string and not a function in the Helmet childrens
  • I use useEffect to cleanup the window.onHelmetLoad when the component is unmounted, as a good practice (no need to keep reference to function of unmounted component), and not to have collision with some other place I am going to use the same approach.

@nonjene
Copy link

nonjene commented Sep 19, 2022

Seems Helmet has transform the onLoad function to inline script.
So we can pass a static function like this:

function scriptOnload(el: HTMLScriptElement){
	const {id, foo} = el.dataset;
	// script initialize with data id & foo
}

<script
	src={url}
	async
	data-id={id}
	data-foo={foo}
	// @ts-ignore - Helmet will transform the onload function to inline script
	onLoad={`(${scriptOnload.toString()})(this)`}
/>

@brianwestphal
Copy link

An approach that seems to work well client side is:

const HelmetScript = ({ src, onLoad }: { src: string; onLoad: () => void }) => {
  const uid = useUuid();

  const onChangeClientState = useCallback(() => {
    const scriptElem = document.getElementById(uid);
    if (scriptElem !== null) {
      scriptElem.onload = onLoad;
    }
  }, [onLoad, uid]);

  return (
    <Helmet onChangeClientState={onChangeClientState}>
      <script id={uid} src={src} async={true} />
    </Helmet>
  );
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests