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

(next/script) onReady 'Undefined' in Dev, Works in Production #39993

Closed
1 task done
jacobgraf opened this issue Aug 27, 2022 · 8 comments · Fixed by #40002
Closed
1 task done

(next/script) onReady 'Undefined' in Dev, Works in Production #39993

jacobgraf opened this issue Aug 27, 2022 · 8 comments · Fixed by #40002
Labels
bug Issue was opened via the bug report template.

Comments

@jacobgraf
Copy link

jacobgraf commented Aug 27, 2022

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.0.0: Mon Aug  1 06:32:20 PDT 2022; root:xnu-8792.0.207.0.6~26/RELEASE_ARM64_T6000
Binaries:
  Node: 16.17.0
  npm: 8.15.0
  Yarn: 1.22.19
  pnpm: N/A
Relevant packages:
  next: 12.2.6-canary.5
  eslint-config-next: 12.2.5
  react: 18.2.0
  react-dom: 18.2.0

What browser are you using? (if relevant)

Brave Version 1.42.97 Chromium: 104.0.5112.102 (Official Build) (arm64)

How are you deploying your application? (if relevant)

Vercel

Describe the Bug

When using the onReady method, the external JS is not available and returns undefined when running in dev mode. It works, as expected, in production build (both locally, and on Vercel)

<div id="contact-form"></div>
<Script
  src="https://www.cognitoforms.com/f/seamless.js"
  data-key="Gz67Fka35Eu5U4y5XthVrg"
  onReady={() => {
    Cognito.mount("216", "#contact-form");
  }}
/>

Expected Behavior

External script resources should be loaded and available before onReady fires.

Link to reproduction

https://github.com/jacobgraf/next-script-onready-issue

To Reproduce

Run development build and load the page. You will receive an Unhandled Runtime Error - ReferenceError: Cognito is not defined.

If you run a production build, Cognito is available when onReady fires and the form loads, as expected.

@jacobgraf jacobgraf added the bug Issue was opened via the bug report template. label Aug 27, 2022
@SukkaW
Copy link
Contributor

SukkaW commented Aug 27, 2022

@jacobgraf

I can reproduce the issue, and I have identified the cause.

next/script's onReady is not concurrent rendering resilient, and you have enabled the reactStrictMode in your next.config.js. So the react strict mode revealed a race condition here (hence your issue). The issue is not happening in production mode, because react strict mode has no effect in production.

It should be a confirmed bug of Next.js.

SukkaW added a commit to SukkaW/next.js that referenced this issue Aug 27, 2022
SukkaW added a commit to SukkaW/next.js that referenced this issue Aug 27, 2022
SukkaW added a commit to SukkaW/next.js that referenced this issue Aug 27, 2022
@SukkaW
Copy link
Contributor

SukkaW commented Aug 27, 2022

I have tried to create a fix for the issue (see #40002), but I am afraid I can't make the test cases pass.

There are some fundamental flaws of next/script components (like next/script could execute side effects directly during the render phase, or there is no way to clear effect declared in useEffect). So basically if you have enabled strict mode, you will eventually hit race condition (like this one you reported) here and there...

For now, I could only recommend you load third-party script manually, without using next/script.

@jacobgraf
Copy link
Author

Dang. I was hopeful! So do you think there will ever be a fix for this issue or is it just going to be an "it is what it is" thing with next/script.

In my example, what would be the recommended way to set this up without next/script so it's more reliable than the setTimeout hack?

@SukkaW
Copy link
Contributor

SukkaW commented Aug 27, 2022

Dang. I was hopeful! So do you think there will ever be a fix for this issue or is it just going to be an "it is what it is" thing with next/script.

In my example, what would be the recommended way to set this up without next/script so it's more reliable than the setTimeout hack?

@jacobgraf And actually, the third-party script you included also introduce some race condition!

image

image

Here I was trying to reproduce and debug your issue. Notice that Cognito is still undefined when CognitoConfiguration becomes an object during onLoad? The Cognito only becomes available during the second onReady call.

The double onReady is the race condition I want to solve in #40002, by avoiding the first onReady call.

@jacobgraf
Copy link
Author

That's a good catch too! I'll ping them and see if they can fix that on their end too!

Any other tips or tricks for getting this fixed in the meantime while I wait for a more permanent fix or is my setTimeout hack the best way for now?

@SukkaW
Copy link
Contributor

SukkaW commented Aug 27, 2022

That's a good catch too! I'll ping them and see if they can fix that on their end too!

Any other tips or tricks for getting this fixed in the meantime while I wait for a more permanent fix or is my setTimeout hack the best way for now?

As you can see above, in development mode, Cognito will be undefined in the first onReady call, and will be available in the second onReady call. In production, there will be only one onReady call that Cognito is already available.

So you can add a falsy value check:

      <Script
        src="https://www.cognitoforms.com/f/seamless.js"
        onReady={() => {
          if (typeof Cognito !== 'undefined') {
            Cognito.mount("216", "#contact-form");
          }
        }}
      />

This will work for now (in both development and production mode) and will continue to work if #40002 is merged.

@kodiakhq kodiakhq bot closed this as completed in #40002 Aug 28, 2022
kodiakhq bot pushed a commit that referenced this issue Aug 28, 2022
Fixes #39993.

Before the PR:

- `next/script` component mount, `useEffect` for `onReady` executes
- The script's cacheKey is not added to `LoadCache`, skip `onReady`
- The second `useEffect` for `loadScript` executes
- The script's cacheKey is added to `LoadCache` even if it might not fully load yet
- Because of React's strict mode, `useEffect` for `onReady` executes again
- Since the script's cacheKey is in `LoadCache`, `onReady` is called (even when the script is not loaded yet)
- After the script is actually loaded, inside the `script.onload` event handler the `onReady` is called again

After the PR:

- `next/script` component mount, `useEffect` for `onReady` executes
- The script's cacheKey is not added to `LoadCache`, `useEffect` skips `onReady`
- The second `useEffect` for `loadScript` executes
- The script's cacheKey is added to `LoadCache` only if it is an inline script
- Because of React's strict mode, `useEffect` for `onReady` executes again
- The script is not yet loaded, its cacheKey is not in `LoadCache`, `useEffect` skips `onReady` again
- After the script is actually loaded, inside the `script.onload` event handler the `onReady` is finally called

In short, the PR resolves a race condition that only occurs under React strict mode (and makes the `next/script` component more concurrent rendering resilient).

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`
@jacobgraf
Copy link
Author

Dang. I was hopeful! So do you think there will ever be a fix for this issue or is it just going to be an "it is what it is" thing with next/script.
In my example, what would be the recommended way to set this up without next/script so it's more reliable than the setTimeout hack?

@jacobgraf And actually, the third-party script you included also introduce some race condition!

image image

Here I was trying to reproduce and debug your issue. Notice that Cognito is still undefined when CognitoConfiguration becomes an object during onLoad? The Cognito only becomes available during the second onReady call.

The double onReady is the race condition I want to solve in #40002, by avoiding the first onReady call.

Your onLoad method had a spelling error. Could this be the cause of the weird behavior?

@github-actions
Copy link
Contributor

This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 29, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Issue was opened via the bug report template.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants