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

Enable forcing useLayoutEffect when using alternate renderers #1685

Closed

Conversation

speakingcode
Copy link
Contributor

useLayoutEffect does not work with server-side rendering, and so a warning is displayed if it is used under Node. To get around this warning, useIsomorphicLayoutEffect tries to detect if the environment is not a browser (via the absence of window.document.createElement, etc) and falls back to useEffect if the check fails. Unfortunately, this may cause issues for alternate renderers that run in Node but are not using server-side rendering, such as react-blessed or Ink, and rely on useLayoutEffect to ensure store subscriptions happen at the appropriate phase of rendering.

This PR makes available a function forceUseLayoutEffect exported from alternate-renderers. A client of the react-redux library can call this to force use of useLayoutEffect in any environment.

import { forceUseLayoutEffect } from 'react-redux/alternate-renderers'
//...

forceUseLayoutEffect()

Implementation notes:

  1. Because calling forceUseLayoutEffect() is dynamic and will happen after all of the module files are evaluated (thanks to ES module imports), a getUseIsomorphicLayoutEffect dynamic getter is provided and existing imports and calls to useIsomorphicLayoutEffect are replaced with calls to the getter
  2. Dynamic importing (import(...)) is enabled via a babel.config.json file, as dynamic importing is necessary to mimic different import-time environment conditions for testing

@netlify
Copy link

netlify bot commented Jan 28, 2021

Deploy preview for react-redux-docs ready!

Built with commit c5925cf

https://deploy-preview-1685--react-redux-docs.netlify.app

@speakingcode
Copy link
Contributor Author

Noticing that in TravisCI a test fails with

TypeError: _vm(...).SyntheticModule is not a constructor

I had this issue locally before providing a babel.config.json to enable dynamic imports. Not sure how to make TravisCI pick this up, or does it need to be configured in a different way (package.json, etc)?

]
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I overlooked that. Let me move that over and push that up..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved the config to .babelrcjs and removed the babel.config.js, but it appears TravisCI is still not picking up that change. Tests run and pass locally..
2021-01-29-132046_660x432_scrot

Copy link
Member

Choose a reason for hiding this comment

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

You should remove the babel changes. The version of Node we use (14.x) already supports import() built-in.

Copy link
Member

Choose a reason for hiding this comment

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

And preset-env will also add the dynamic import syntax by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is needed for Jest. It is described in Jest documentation https://jestjs.io/docs/en/webpack.html#using-with-webpack-2
It is also discussed in this issue: jestjs/jest#2442

In that thread I just noticed this comment, which may be why TravisCI is failing but the tests run fine locally:
jestjs/jest#2442 (comment)

My tests were failing in CI but not locally the reason for this being that the NODE_ENV was set to production in the Dockerfile. The .babelrc env property is matched against the BABEL_ENV environment variable. Failing that it uses NODE_ENV. Jest will set NODE_ENV to "test" if it is null! Therefore because it was production it wouldn't set it to test and babel would not run the transform.

Comment on lines +23 to +24
// Supply a getter just to skip dealing with ESM bindings
export const getUseIsomorphicLayoutEffect = () => useIsomorphicLayoutEffect
Copy link
Member

Choose a reason for hiding this comment

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

This shouldn't be needed. While imported values are treated as read-only, exported values can change within the module. So, calling forceUseLayoutEffect() should be fine for the entirety of the execution context.

I would add a forceUseEffect() as well to allow setting it back.

Copy link
Contributor Author

@speakingcode speakingcode Jan 29, 2021

Choose a reason for hiding this comment

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

I was under the impression that exported values should change in the importer when changed within the module, but I didn't see that behavior in testing - perhaps an issue in babel? The getBatch getter exported from utils/reactBatchedUpdates.js was provided for that exact reason (according to @markerikson).

Consider the following test cases. In each, the module is loaded in a beforeEach block with no window.document.createElement (thus setting useIsomorphicLayoutEffect to useEffect). Calling forceUseLayoutEffect after the module is imported, useIsomorphicLayoutEffect is useEffect unless 1.) the module is re-imported or 2. The getter is used.
this works as expected:

      it('forces useIsomorphicLayoutEffect to useLayoutEffect', async () => {
        forceUseLayoutEffect()
        ;({ useIsomorphicLayoutEffect } = await import(
          '../../src/utils/useIsomorphicLayoutEffect'
        ))

        expect(useIsomorphicLayoutEffect).toBe(useLayoutEffect)

but fails if the re-import is omitted:

      it('forces useIsomorphicLayoutEffect to useLayoutEffect', async () => {
        forceUseLayoutEffect()
        //;({ useIsomorphicLayoutEffect } = await import(
        //  '../../src/utils/useIsomorphicLayoutEffect'
        //))

        expect(useIsomorphicLayoutEffect).toBe(useLayoutEffect)

->

    Expected: [Function useLayoutEffect]
    Received: [Function useEffect]

whereas it passes with the getter:

      it('forces useIsomorphicLayoutEffect to useLayoutEffect', async () => {
        forceUseLayoutEffect()
       
        expect(getUseIsomorphicLayoutEffect()).toBe(useLayoutEffect)
      })

The same behavior was observed when using the "static" import { useIsomorphicLayoutEffect } from ...

@timdorr
Copy link
Member

timdorr commented Feb 1, 2021

This is impressively causing Node to segfault, so is there some looping going on in the tests? That's usually the cause of those kinds of hard crashes.

@markerikson Did you want to take a look at this? I consider this more "your" code, since you implemented it originally IIRC.

@markerikson
Copy link
Contributor

Yeah, I asked for this PR, and i do want to look it over. I've just got a lot going on atm and don't have the mental capacity to do so right now. Will try to, Soon (TM) :)

@speakingcode
Copy link
Contributor Author

It is strange that it's seg faulting, before the error was TypeError: _vm(...).SyntheticModule is not a constructor (which I also experienced locally when babel-plugin-dynamic-import-node was not ini the env:test config in .babelrc). There are no loops or circular logic, and the additional test coverage for forceUseEffect is structurally identical to forceUseLayoutEffect. That said upon looking I noticed I forgot to export forceUseEffect from alternate-renderers, and additionally the @babel/plugin-syntax-dynamic-import plugins entry is not necessary, only the babel-plugin-dynamic-import-node entry in env.test.plugins so I'll push those changes

@markerikson
Copy link
Contributor

My inclination here is that we don't want to allow switching between useLayoutEffect and useEffect after the app has started up. I don't see any benefit to doing so, and it feels potentially confusing. Also, right now, we're adding in a couple extra function calls every time even though the odds of this ever changing are practically zero.

Problem is, I'm not sure how to restrict that. If we call the getter before the hook is defined, then the file probably gets imported and loaded well before your entry point has a chance to import and call forceUseLayoutEffect(), and the call sorta ends up being useless because we already got a handle to useEffect before you could override that.

I suppose we could add yet another ref to store that hook reference, but it feels like a waste.

@markerikson
Copy link
Contributor

Heh, this never did get merged. I believe it's obsolete as of v8 because we switched to useSyncExternalStore.

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.

3 participants