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

Add fallback to setImmediate for Expo Router static rendering #4665

Merged
merged 8 commits into from
Sep 5, 2023

Conversation

javascripter
Copy link
Contributor

Summary

In Node.js environments such as when static rendering with Expo Router, requestAnimationFrame is not defined. This results in an error when using libraries that depend on react-native-reanimated like Drawer or when using react-native-reanimated directly in Expo Router with static rendering, causing a hydration mismatch due to rendering errors.

In this PR, I added a fallback to setImmediate if requestAnimationFrame is not defined in globalThis. I did not simply replace requestAnimationFrame with setImmediate because setImmediate is slower than requestAnimationFrame in CSR environments. This issue is not applicable to static rendering environments however, so setImmediate is fine there.

Test plan

@javascripter
Copy link
Contributor Author

Not very sure how to fix failing jest tests.

@satya164
Copy link
Contributor

satya164 commented Jul 3, 2023

Seems like the actual issue is that scheduleOnUI is being called during render for SSR/static build - which results in calling requestAnimationFrame. Side effects and callbacks shouldn't be called during SSR/static build.

@javascripter
Copy link
Contributor Author

javascripter commented Jul 4, 2023

Seems like the actual issue is that scheduleOnUI is being called during render for SSR/static build - which results in calling requestAnimationFrame. Side effects and callbacks shouldn't be called during SSR/static build.

@satya164 I see. If I change the code to the below tests will pass. Do you think the below approach seems more reasonable? If so, I will update both the code and the PR description accordingly.

  scheduleOnUI<T>(worklet: ShareableRef<T>) {
    // In Node.js environments (like when static rendering with Expo Router)
    // requestAnimationFrame is unavailable. Do not call side effects in this case.
    if (typeof requestAnimationFrame === 'undefined') {
      return;
    }
    // @ts-ignore web implementation has still not been updated after the rewrite, this will be addressed once the web implementation updates are ready
    requestAnimationFrame(worklet);
  }

@tjzel
Copy link
Collaborator

tjzel commented Jul 5, 2023

I've tried to understand what's causing the jest problem but still haven't got any solid results.

From what I concluded if we use scheduleOnUI (which comes from scheduleOnUI = requestAnimationFrame) in scheduleOnUI (btw. I think it's a bad idea to have the same name for those) it's causing tests to fail, but if we call directly requestAnimationFrame in scheduleOnUI it's ok.
Probably because scheduleOnUI gets torn down (or torn down before) requestAnimationFrame and is still called? I haven't yet had a great deal of experience of solving Jest issues so I'm not exactly sure what that means.

@satya164
Copy link
Contributor

satya164 commented Jul 5, 2023

@javascripter: Do you think the below approach seems more reasonable?

Imo it doesn't address the root of the problem - scheduleOnUI being called during SSR/static build

@nDeavor9
Copy link

nDeavor9 commented Aug 7, 2023

Sorry to directly comment on this.
Can you guys please let me know when will this PR be merged?
I'm facing the ReferenceError: requestAnimationFrame is not defined at JSReanimated.scheduleOnUI and it's blocking my release.
Hopefully it gets done soon. Thanks.

@arivanbastos
Copy link

Hi guys. Any workaround to this problem?
Cant release my app due "ReferenceError: requestAnimationFrame is not defined" error.

@tomekzaw
Copy link
Member

tomekzaw commented Aug 9, 2023

Hey @nDeavor9 and @arivanbastos, can you confirm if this PR resolves the issue?

@nDeavor9
Copy link

nDeavor9 commented Aug 9, 2023

@tomekzaw I guess so.
As @satya164 said, the root problem might be different, but the error that's blocking from exporting is exactly 'requestAnimationFrame' being undefined.

@sabyasachibose-sigfyn
Copy link

Our builds are blocked at the moment. Can we please have a resolution on this?

@arivanbastos
Copy link

arivanbastos commented Aug 9, 2023

@tomekzaw
Edit
I installed the PR by running
npm i software-mansion/react-native-reanimated#pull/4665

And yes, the error is gone.

@javascripter
Copy link
Contributor Author

javascripter commented Aug 9, 2023

Sorry, haven't had much time to investigate the root cause further, since I'm not very familiar with reanimated's internals.

I originally posted the patch on Twitter with minimal changes, so if you're blocked on this you can use pnpm patch or patch-package to workaround this issue as well.

Twitter post:
https://twitter.com/javascripter/status/1675549640471760896
Link to patch:
https://gist.github.com/javascripter/4e4e20e9024a33592437648b718c763d

@bhyoo99
Copy link

bhyoo99 commented Aug 11, 2023

Here is my patch-package file
https://gist.github.com/bhyoo99/f4954316c498f3ca261197166855e21d

✅ expo-router checked
✅ SSG web build checked

@tjzel
Copy link
Collaborator

tjzel commented Aug 22, 2023

Hi @javascripter! I took the liberty to commit to your fork, since I got a solution for this that should both work and pass our tests. Turns out at some point in our code (but after creating scheduleOnUI) we substitute requestAnimationFrame implementation accordingly to how it should be done with React Native when using Jest. Since it's happening later, scheduleOnUI would not have it's implementation substituted.

Please let me know if it works for you - if it does, I think it is safe to merge (after my colleagues review it of course).

Comment on lines 14 to 18
const scheduleOnUI = globalThis.requestAnimationFrame
? isJest()
? mockedRequestAnimationFrame
: globalThis.requestAnimationFrame
: setImmediate;
Copy link
Member

@tomekzaw tomekzaw Aug 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. First, can we handle Jest first and then the rest?
const scheduleOnUI = isJest()
  ? mockedRequestAnimationFrame
  : globalThis.requestAnimationFrame || globalThis.setImmediate;
  1. Can we add a new platform checker isStaticRendering()? This way we could write it this way:
const scheduleOnUI = isJest()
  ? mockedRequestAnimationFrame
  : isStaticRendering()
  ? globalThis.setImmediate
  : globalThis.requestAnimationFrame;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. As far as I understand in this environment useAnimatedFrame is unavailable in Jest also and we want to use setImmediate there. With this implementation we would always use the mock - maybe it's desired - @javascripter could you give more insight on that?

  2. This is hard, we have to discuss it tomorrow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used setImmediate in the static rendering environment simply because I tested it in my environment and it seemed to work well for me. As @satya164 said, scheduleOnUI probably doesn't need to execute in that environment so I don't have a concrete reason to prefer setImmediate over the mock in Jest. Always using the mock in Jest seems a reasonable approach to me. @tjzel

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Can we check who exactly calls scheduleOnUI here any why? Maybe we can move the call into a conditional. I suspect this might be initializeUIRuntime which calls runOnUIImmediately.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually had a quite fruitful discussion about this and we want to go a step further and improve how Reanimated behaves during Static Renders. This PR can stay as a workaround for the moment, but we will open a proper one in the near future.

@javascripter
Copy link
Contributor Author

Hi @javascripter! I took the liberty to commit to your fork, since I got a solution for this that should both work and pass our tests. Turns out at some point in our code (but after creating scheduleOnUI) we substitute requestAnimationFrame implementation accordingly to how it should be done with React Native when using Jest. Since it's happening later, scheduleOnUI would not have it's implementation substituted.

Please let me know if it works for you - if it does, I think it is safe to merge (after my colleagues review it of course).

@tjzel Hi. Thanks for taking a look at this! Yes, this works for me, and the commit looks good to me as well.

@kevinhylant
Copy link

Looks like there's a conflict — hoping this will get merged soon-ish :)

Thanks for all your work here!

global.requestAnimationFrame = (callback: (timestamp: number) => void) => {
return setTimeout(() => callback(performance.now()), 0);
};
global.requestAnimationFrame = mockedRequestAnimationFrame;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we check globalThis.requestAnimationFrame, shouldn't we also assign to globalThis instead of global?

Suggested change
global.requestAnimationFrame = mockedRequestAnimationFrame;
globalThis.requestAnimationFrame = mockedRequestAnimationFrame;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our use cases it should be irrelevant what we assign it to according to some docs but I guess it's better practice to use globalThis.

Comment on lines 14 to 18
const scheduleOnUI = globalThis.requestAnimationFrame
? isJest()
? mockedRequestAnimationFrame
: globalThis.requestAnimationFrame
: setImmediate;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Can we check who exactly calls scheduleOnUI here any why? Maybe we can move the call into a conditional. I suspect this might be initializeUIRuntime which calls runOnUIImmediately.

@@ -28,7 +38,7 @@ export default class JSReanimated {

scheduleOnUI<T>(worklet: ShareableRef<T>) {
// @ts-ignore web implementation has still not been updated after the rewrite, this will be addressed once the web implementation updates are ready
requestAnimationFrame(worklet);
_scheduleOnUI(worklet);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our codebase the leading underscore means that the function is a JSI binding. Can we rename it to something else?

@tjzel tjzel added this pull request to the merge queue Sep 5, 2023
Merged via the queue into software-mansion:main with commit bf59966 Sep 5, 2023
hyochan added a commit to hyochan/expo-router-starter that referenced this pull request Nov 2, 2023
https://blog.expo.dev/expo-sdk-49-c6d398cdf740

Having issue in web
- expo/expo#23412
- software-mansion/react-native-reanimated#4665

## Issue

Upgrading expo metro as suggested in
expo/router#864 (comment) causes
problem when running app in `iOS` (not yet tested in android). It
freezes in `SplashScreen`. So we need to becareful in merging this `PR`.
Currently, it is only working in `web` and freezes in `iOS`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants