react-helmet icon indicating copy to clipboard operation
react-helmet copied to clipboard

How do I know the scripts are ready?

Open gocreating opened this issue 8 years ago • 22 comments

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

gocreating avatar May 22 '16 18:05 gocreating

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.

potench avatar May 22 '16 21:05 potench

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.

romanonthego avatar Jun 03 '16 08:06 romanonthego

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

DavidWells avatar Oct 27 '16 19:10 DavidWells

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

navgarcha avatar Jan 09 '17 20:01 navgarcha

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)
  })
}

romanonthego avatar Jan 10 '17 09:01 romanonthego

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)}
/>

navgarcha avatar Jan 10 '17 11:01 navgarcha

@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.

danielkcz avatar Jan 18 '17 12:01 danielkcz

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

navgarcha avatar Jan 18 '17 12:01 navgarcha

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.

danielkcz avatar Jan 24 '17 10:01 danielkcz

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.

roastlechon avatar May 24 '17 06:05 roastlechon

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.

smileart avatar Mar 21 '18 14:03 smileart

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

JakobJingleheimer avatar Mar 27 '18 11:03 JakobJingleheimer

https://github.com/nfl/react-helmet/pull/299 This PR seems to address this issue, but has not been merged yet :(

mcabrams avatar May 21 '18 23:05 mcabrams

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.

fitfinderaustralia avatar Dec 16 '18 11:12 fitfinderaustralia

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.

PEM-- avatar Jul 22 '19 13:07 PEM--

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: https://github.com/nfl/react-helmet/issues/328

suhanw avatar Jan 29 '20 22:01 suhanw

Yes - ignore.

Le lun. 22 juil. 2019 à 23:35, Pierre-Eric Marchandet < [email protected]> a écrit :

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' && } </Helmet> {scriptLoaded && ...} </> }

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

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/nfl/react-helmet/issues/146?email_source=notifications&email_token=AKNGXKHDT63TKRR5N3IDKDTQAWZQXA5CNFSM4CEQGIGKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD2P54XA#issuecomment-513793628, or mute the thread https://github.com/notifications/unsubscribe-auth/AKNGXKFABVJVRXS2F5CRVPTQAWZQXANCNFSM4CEQGIGA .

fitfinderaustralia avatar Jan 29 '20 22:01 fitfinderaustralia

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

sedatbasar avatar Oct 13 '20 08:10 sedatbasar

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.

khitrenovich avatar Aug 25 '21 14:08 khitrenovich

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.

a-tonchev avatar Nov 09 '21 10:11 a-tonchev

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)`}
/>

nonjene avatar Sep 19 '22 07:09 nonjene