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

[Fast Refresh] Support callthrough HOCs #21104

Merged
merged 8 commits into from
Mar 30, 2021
Merged

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Mar 26, 2021

Fixes #20417.

See explanation of the problem in #20417 (comment). The fix I settled on (see next commits) is to add signatures to all HOC wrappers above, not just the inner function. If they already had signatures, they would be ignored. But if not, this is our chance to tag them so that if they call our component as a function directly (instead of rendering it), we take its list of Hooks into account.

This still won't solve the problem for more convoluted cases, like if you're developing a HOC that does callthrough. Because then the transform will for it too, and fill its signature first. But that seems like a reasonable compromise since the whole pattern is shady. Let's just get the MobX case working.

@sizebot
Copy link

sizebot commented Mar 26, 2021

Comparing: 1b7e471...00178db

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 122.93 kB 122.93 kB = 39.53 kB 39.53 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js = 129.45 kB 129.45 kB = 41.61 kB 41.61 kB
facebook-www/ReactDOM-prod.classic.js = 407.25 kB 407.25 kB = 75.66 kB 75.67 kB
facebook-www/ReactDOM-prod.modern.js = 395.51 kB 395.51 kB = 73.72 kB 73.72 kB
facebook-www/ReactDOMForked-prod.classic.js = 407.25 kB 407.25 kB = 75.67 kB 75.67 kB
oss-experimental/react-refresh/cjs/react-refresh-babel.production.min.js +3.92% 7.20 kB 7.48 kB +3.14% 2.58 kB 2.66 kB
oss-stable/react-refresh/cjs/react-refresh-babel.production.min.js +3.92% 7.20 kB 7.48 kB +3.14% 2.58 kB 2.66 kB
oss-experimental/react-refresh/cjs/react-refresh-babel.development.js +3.54% 24.12 kB 24.97 kB +3.63% 5.51 kB 5.71 kB
oss-stable/react-refresh/cjs/react-refresh-babel.development.js +3.54% 24.12 kB 24.97 kB +3.63% 5.51 kB 5.71 kB
facebook-www/ReactFreshRuntime-dev.classic.js +3.49% 21.62 kB 22.38 kB +2.48% 6.38 kB 6.54 kB
facebook-www/ReactFreshRuntime-dev.modern.js +3.49% 21.62 kB 22.38 kB +2.48% 6.38 kB 6.54 kB
oss-experimental/react-refresh/cjs/react-refresh-runtime.development.js +3.36% 21.52 kB 22.24 kB +2.33% 6.36 kB 6.50 kB
oss-stable/react-refresh/cjs/react-refresh-runtime.development.js +3.36% 21.52 kB 22.24 kB +2.33% 6.36 kB 6.50 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-refresh/cjs/react-refresh-babel.production.min.js +3.92% 7.20 kB 7.48 kB +3.14% 2.58 kB 2.66 kB
oss-stable/react-refresh/cjs/react-refresh-babel.production.min.js +3.92% 7.20 kB 7.48 kB +3.14% 2.58 kB 2.66 kB
oss-experimental/react-refresh/cjs/react-refresh-babel.development.js +3.54% 24.12 kB 24.97 kB +3.63% 5.51 kB 5.71 kB
oss-stable/react-refresh/cjs/react-refresh-babel.development.js +3.54% 24.12 kB 24.97 kB +3.63% 5.51 kB 5.71 kB
facebook-www/ReactFreshRuntime-dev.classic.js +3.49% 21.62 kB 22.38 kB +2.48% 6.38 kB 6.54 kB
facebook-www/ReactFreshRuntime-dev.modern.js +3.49% 21.62 kB 22.38 kB +2.48% 6.38 kB 6.54 kB
oss-experimental/react-refresh/cjs/react-refresh-runtime.development.js +3.36% 21.52 kB 22.24 kB +2.33% 6.36 kB 6.50 kB
oss-stable/react-refresh/cjs/react-refresh-runtime.development.js +3.36% 21.52 kB 22.24 kB +2.33% 6.36 kB 6.50 kB

Generated by 🚫 dangerJS against 00178db

This shows why my initial approach doesn't make sense.
_s();
const [foo, setFoo] = useState(0);
React.useEffect(() => {});
return <h1 ref={ref}>{foo}</h1>;
})), "useState{[foo, setFoo](0)}\\nuseEffect{}");
}, "useState{[foo, setFoo](0)}\\nuseEffect{}")), "useState{[foo, setFoo](0)}\\nuseEffect{}")), "useState{[foo, setFoo](0)}\\nuseEffect{}");
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This adds a bunch of duplication to the output. But the real one is hashed. Also it's only for HOCs which are out of favor. So meh.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also it's only for HOCs which are out of favor. So meh.

Then again, isn't this change itself to support an even more uncommon use case (a HOC like pattern that directly calls a component as a function)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well, HOCs are not that uncommon.

In the original case, we have a roughly 7% usage library which downright causes a crash every time a component is edited, in default setups with CRA/Next/RN. Pretty disruptive and they don't have a fix without giving up on the whole approach.

I'm just saying that the added output duplication does not make the remaining cases worse. It just slightly bloats the DEV-only output.

@gaearon
Copy link
Collaborator Author

gaearon commented Mar 26, 2021

ok I think this approach works

@yuhongda
Copy link

Fixes #20417.

See explanation of the problem in #20417 (comment). The fix I'm making here is to mark the outermost HOC return value with the signature instead of the passed function itself mark all levels of returned HOCs with the passed function's signature instead of only doing it for the function itself.

This is not an ideal fix though. Like, what if your HOC actually does render the inner type, but its own return value is not a component? E.g. an object like {Provider, Consumer}. Then we'll introduce the same exact problem we were trying to avoid, but for different (arguably just as exotic) cases. Because now those components wouldn't be remounted.

We could maybe tweak the contract so that each HOC call level gets the signature derived from the inner one. Although this won't work with our current heuristic of "first call to _s is the wrapper, second call is a signal to collect custom Hooks". That heuristic is pretty confusing anyway though and we could instead look at the arguments (no arguments = time to collect signatures). Maybe that would be a better fix.

Yeah it's better so I pushed that instead.

Great! Can’t wait to try it!

Copy link
Contributor

@bvaughn bvaughn left a comment

Choose a reason for hiding this comment

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

I looked at the new tests and the changed snapshots and they look good 👍🏼

I have not thought deeply about what types of edge-case patterns this change might break.

It don’t see how it would break render prop usage. I’d need a more specific example to explain why it doesn’t.

Normally I would want to test FR with these changes against a project with react-virtualized or something that makes use of other direct component calls (via render props) but I think that pattern would already be fundamentally broken b'c hooks wouldn't be allowed inside of a one-to-many render prop scenario like react-virtualized.

That being said, I wrote a test anyway where The row-render-prop functions returned a React element that itself used hooks, and that test failed (but also failed on master) so maybe that's fine 😁

This is not a case that is important for Fast Refresh, but we shouldn't change the code semantics. This case shows the transform isn't quite correct. It's wrapping the call at the wrong place.
This fixes a false positive that was causing an IIFE to be wrapped in the wrong place, which made the wrapping unsafe.
@gaearon gaearon merged commit 516b76b into facebook:master Mar 30, 2021
@gaearon
Copy link
Collaborator Author

gaearon commented Mar 30, 2021

Let's give this a try. I fixed a corner case I found at FB but haven't seen other issues. Something like #21139 would be a better longer-term fix.

@Jack-Works
Copy link
Contributor

Should I also implement this for react-refresh-typescript?

@gaearon
Copy link
Collaborator Author

gaearon commented Apr 1, 2021

Yeah I guess that would be nice.

_c3 = A;
export const B = React.memo(_c5 = React.forwardRef(_c4 = _s2(function (props, ref) {
export const B = _s2(React.memo(_c5 = _s2(React.forwardRef(_c4 = _s2(function (props, ref) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm porting this PR to the TypeScript version.
I aggressively detect if the function needs to be hashed appears at the callee position of the CallExpression (which means only literal IIFE ((() => {})(...args))) instead of allowing it appears in the argument position (memo(f)). Is that OK?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure I understand the question.

Copy link
Contributor

Choose a reason for hiding this comment

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

So in this PR, it seems there are some irrelated (or false positive) changes in the snapshot:

https://github.com/facebook/react/pull/21104/files/00178dbaf7b5af453807a9d57724568b7c67759b?file-filters%5B%5D=.snap#diff-946346b4b3bb1833d001b16c63e3b733545cb5bbc651c2bfb25c37f664fd1a6dL40-R46

And in the TS version there are no such snapshot changes, only the new test case

while (item) {
    ;((item) => {
        useFoo()
    })(item)
}

I'd like to know if those changes are really false positive or I should port those behaviors to make it work correctly?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll release it for now

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why do you call these changes false positive? Can you describe what you expected vs what you saw?

Copy link
Contributor

Choose a reason for hiding this comment

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

So as the PR title describes, it add support for IIFEs. But these baseline changes are not IIFEs, just a normal function call

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think there's a misunderstanding.

The change in this PR has nothing to do with IIFEs. It only relates to higher-order components that call rather than render the passed function.

The IIFE test case is not something that's related to Hooks or Fast Refresh. It's just that we had a case like this in the codebase that was accidentally getting detected as a Hook call (because of the use prefix). My change broke the semantics of that code. So I added a regression test that verifies that code like this does not get broken.

Copy link
Contributor

Choose a reason for hiding this comment

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

So what's the actual test case? Can you give a minimal example so I can fix that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The test showing incorrect output is d2a2045. Then 070262b shows the fix and the correct output.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Concretely, the problem was that walking upwards was only correct as long as the inner node represents the argument. If the child node was the callee itself we should stop walking upwards.

Jack-Works added a commit to Jack-Works/react-refresh-transformer that referenced this pull request Apr 5, 2021
@Jack-Works
Copy link
Contributor

Implemented in https://github.com/Jack-Works/react-refresh-transformer/pull/5/files. but I still have some question about this PR posted in #21104 (comment)

facebook-github-bot pushed a commit to facebook/react-native that referenced this pull request Apr 6, 2021
Summary:
This sync includes the following changes:
- **[c9aab1c9d](facebook/react@c9aab1c9d )**: react-refresh@0.10.0 //<Dan Abramov>//
- **[516b76b9a](facebook/react@516b76b9a )**: [Fast Refresh] Support callthrough HOCs ([#21104](facebook/react#21104)) //<Dan Abramov>//
- **[0853aab74](facebook/react@0853aab74 )**: Log all errors to console.error by default ([#21130](facebook/react#21130)) //<Sebastian Markbåge>//
- **[d1294c9d4](facebook/react@d1294c9d4 )**: Add global onError handler ([#21129](facebook/react#21129)) //<Sebastian Markbåge>//
- **[64983aab5](facebook/react@64983aab5 )**: Remove redundant setUpdatePriority call ([#21127](facebook/react#21127)) //<Andrew Clark>//
- **[634cc52e6](facebook/react@634cc52e6 )**: Delete dead variable: currentEventWipLanes ([#21123](facebook/react#21123)) //<Andrew Clark>//
- **[1102224bb](facebook/react@1102224bb )**: Fix: flushSync changes priority inside effect ([#21122](facebook/react#21122)) //<Andrew Clark>//
- **[dbe98a5aa](facebook/react@dbe98a5aa )**: Move sync task queue to its own module ([#21109](facebook/react#21109)) //<Andrew Clark>//
- **[3ba5c8737](facebook/react@3ba5c8737 )**: Remove Scheduler indirection ([#21107](facebook/react#21107)) //<Andrew Clark>//
- **[46b68eaf6](facebook/react@46b68eaf6 )**: Delete LanePriority type ([#21090](facebook/react#21090)) //<Andrew Clark>//
- **[dcd13045e](facebook/react@dcd13045e )**: Use Lane to track root callback priority ([#21089](facebook/react#21089)) //<Andrew Clark>//
- **[5f21a9fca](facebook/react@5f21a9fca )**: Clean up host pointers in level 2 of clean-up flag ([#21112](facebook/react#21112)) //<Andrew Clark>//
- **[32d6f39ed](facebook/react@32d6f39ed )**: [Fizz] Support special HTML/SVG/MathML tags to suspend ([#21113](facebook/react#21113)) //<Sebastian Markbåge>//
- **[a77dd13ed](facebook/react@a77dd13ed )**: Delete enableDiscreteEventFlushingChange ([#21110](facebook/react#21110)) //<Andrew Clark>//
- **[048ee4c0c](facebook/react@048ee4c0c )**: Use `act` in fuzz tester to flush expired work ([#21108](facebook/react#21108)) //<Andrew Clark>//
- **[556644e23](facebook/react@556644e23 )**: Fix plurals ([#21106](facebook/react#21106)) //<Sebastian Markbåge>//
- **[8b741437b](facebook/react@8b741437b )**: Rename SuspendedWork to Task ([#21105](facebook/react#21105)) //<Sebastian Markbåge>//
- **[38a1aedb4](facebook/react@38a1aedb4 )**: [Fizz] Add FormatContext and Refactor Work ([#21103](facebook/react#21103)) //<Sebastian Markbåge>//
- **[1b7e471b9](facebook/react@1b7e471b9 )**: React Native New Architecture: Support passing nativeViewTag to getInspectorDataForViewAtPoint callback, for React DevTools compat ([#21080](facebook/react#21080)) //<Joshua Gross>//
- **[4a99c5c3a](facebook/react@4a99c5c3a )**: Use highest priority lane to detect interruptions ([#21088](facebook/react#21088)) //<Andrew Clark>//
- **[77be52729](facebook/react@77be52729 )**: Remove LanePriority from computeExpirationTime ([#21087](facebook/react#21087)) //<Andrew Clark>//
- **[3221e8fba](facebook/react@3221e8fba )**: Remove LanePriority from getBumpedLaneForHydration ([#21086](facebook/react#21086)) //<Andrew Clark>//
- **[05ec0d764](facebook/react@05ec0d764 )**: Entangled expired lanes with SyncLane ([#21083](facebook/react#21083)) //<Andrew Clark>//
- **[03ede83d2](facebook/react@03ede83d2 )**: Use EventPriority to track update priority ([#21082](facebook/react#21082)) //<Andrew Clark>//
- **[a63f0953b](facebook/react@a63f0953b )**: Delete SyncBatchedLane ([#21061](facebook/react#21061)) //<Ricky>//
- **[fa868d6be](facebook/react@fa868d6be )**: Make opaque EventPriority type a Lane internally ([#21065](facebook/react#21065)) //<Andrew Clark>//
- **[eb58c3909](facebook/react@eb58c3909 )**: react-hooks/exhaustive-deps: Handle optional chained methods as dependency ([#20204](facebook/react#20204)) ([#20247](facebook/react#20247)) //<Ari Perkkiö>//
- **[7b84dbd16](facebook/react@7b84dbd16 )**: Fail build on deep requires in npm packages ([#21063](facebook/react#21063)) //<Dan Abramov>//
- **[2c9d8efc8](facebook/react@2c9d8efc8 )**: Add react-reconciler/constants entry point ([#21062](facebook/react#21062)) //<Dan Abramov>//
- **[d0eaf7829](facebook/react@d0eaf7829 )**: Move priorities to separate import to break cycle ([#21060](facebook/react#21060)) //<Andrew Clark>//
- **[435cff986](facebook/react@435cff986 )**: [Fizz] Expose callbacks in options for when various stages of the content is done ([#21056](facebook/react#21056)) //<Sebastian Markbåge>//
- **[25bfa287f](facebook/react@25bfa287f )**: [Experiment] Add feature flag for more aggressive memory clean-up of deleted fiber trees ([#21039](facebook/react#21039)) //<Benoit Girard>//
- **[8fe7810e7](facebook/react@8fe7810e7 )**: Remove already completed comment ([#21054](facebook/react#21054)) //<Sebastian Markbåge>//
- **[6c3202b1e](facebook/react@6c3202b1e )**: [Fizz] Use identifierPrefix to avoid conflicts within the same response ([#21037](facebook/react#21037)) //<Sebastian Markbåge>//
- **[dcdf8de7e](facebook/react@dcdf8de7e )**: Remove discrete lanes and priorities ([#21040](facebook/react#21040)) //<Andrew Clark>//
- **[ca99ae97b](facebook/react@ca99ae97b )**: Replace some flushExpired callsites ([#20975](facebook/react#20975)) //<Ricky>//
- **[1fafac002](facebook/react@1fafac002 )**: Use SyncLane for discrete event hydration ([#21038](facebook/react#21038)) //<Andrew Clark>//

Changelog:
[General][Changed] - React Native sync for revisions 6d3ecb7...c9aab1c

jest_e2e[run_all_tests]

Reviewed By: JoshuaGross

Differential Revision: D27436763

fbshipit-source-id: da79a41e26bffdcdacd293178062edf098e9b58a
Jack-Works added a commit to Jack-Works/react-refresh-transformer that referenced this pull request Apr 10, 2021
acdlite pushed a commit to acdlite/react that referenced this pull request Apr 11, 2021
* [Fast Refresh] Support callthrough HOCs

* Add a newly failing testing to demonstrate the flaw

This shows why my initial approach doesn't make sense.

* Attach signatures at every nesting level

* Sign nested memo/forwardRef too

* Add an IIFE test

This is not a case that is important for Fast Refresh, but we shouldn't change the code semantics. This case shows the transform isn't quite correct. It's wrapping the call at the wrong place.

* Find HOCs above more precisely

This fixes a false positive that was causing an IIFE to be wrapped in the wrong place, which made the wrapping unsafe.

* Be defensive against non-components being passed to setSignature

* Fix lint
acdlite pushed a commit to acdlite/react that referenced this pull request Apr 13, 2021
* [Fast Refresh] Support callthrough HOCs

* Add a newly failing testing to demonstrate the flaw

This shows why my initial approach doesn't make sense.

* Attach signatures at every nesting level

* Sign nested memo/forwardRef too

* Add an IIFE test

This is not a case that is important for Fast Refresh, but we shouldn't change the code semantics. This case shows the transform isn't quite correct. It's wrapping the call at the wrong place.

* Find HOCs above more precisely

This fixes a false positive that was causing an IIFE to be wrapped in the wrong place, which made the wrapping unsafe.

* Be defensive against non-components being passed to setSignature

* Fix lint
acdlite pushed a commit to acdlite/react that referenced this pull request Apr 16, 2021
* [Fast Refresh] Support callthrough HOCs

* Add a newly failing testing to demonstrate the flaw

This shows why my initial approach doesn't make sense.

* Attach signatures at every nesting level

* Sign nested memo/forwardRef too

* Add an IIFE test

This is not a case that is important for Fast Refresh, but we shouldn't change the code semantics. This case shows the transform isn't quite correct. It's wrapping the call at the wrong place.

* Find HOCs above more precisely

This fixes a false positive that was causing an IIFE to be wrapped in the wrong place, which made the wrapping unsafe.

* Be defensive against non-components being passed to setSignature

* Fix lint
acdlite pushed a commit to acdlite/react that referenced this pull request Apr 16, 2021
* [Fast Refresh] Support callthrough HOCs

* Add a newly failing testing to demonstrate the flaw

This shows why my initial approach doesn't make sense.

* Attach signatures at every nesting level

* Sign nested memo/forwardRef too

* Add an IIFE test

This is not a case that is important for Fast Refresh, but we shouldn't change the code semantics. This case shows the transform isn't quite correct. It's wrapping the call at the wrong place.

* Find HOCs above more precisely

This fixes a false positive that was causing an IIFE to be wrapped in the wrong place, which made the wrapping unsafe.

* Be defensive against non-components being passed to setSignature

* Fix lint
acdlite pushed a commit to acdlite/react that referenced this pull request Apr 19, 2021
* [Fast Refresh] Support callthrough HOCs

* Add a newly failing testing to demonstrate the flaw

This shows why my initial approach doesn't make sense.

* Attach signatures at every nesting level

* Sign nested memo/forwardRef too

* Add an IIFE test

This is not a case that is important for Fast Refresh, but we shouldn't change the code semantics. This case shows the transform isn't quite correct. It's wrapping the call at the wrong place.

* Find HOCs above more precisely

This fixes a false positive that was causing an IIFE to be wrapped in the wrong place, which made the wrapping unsafe.

* Be defensive against non-components being passed to setSignature

* Fix lint
Jack-Works added a commit to Jack-Works/react-refresh-transformer that referenced this pull request May 22, 2021
Ogg70 pushed a commit to Ogg70/react-refresh-transformer that referenced this pull request May 22, 2021
koto pushed a commit to koto/react that referenced this pull request Jun 15, 2021
* [Fast Refresh] Support callthrough HOCs

* Add a newly failing testing to demonstrate the flaw

This shows why my initial approach doesn't make sense.

* Attach signatures at every nesting level

* Sign nested memo/forwardRef too

* Add an IIFE test

This is not a case that is important for Fast Refresh, but we shouldn't change the code semantics. This case shows the transform isn't quite correct. It's wrapping the call at the wrong place.

* Find HOCs above more precisely

This fixes a false positive that was causing an IIFE to be wrapped in the wrong place, which made the wrapping unsafe.

* Be defensive against non-components being passed to setSignature

* Fix lint
@danieloprado
Copy link

I'm facing a stranger bug may related with this: vitejs/vite-plugin-react#6

When I change the styled doesn't change anything, I also tried a custom HOC and same thing happen. But when I change the syntax to make the const have the wrapped value of styled it worked.

NodePath {
  contexts: [
    TraversalContext {
      queue: [Array],
      priorityQueue: [],
      parentPath: [NodePath],
      scope: [Scope],
      state: undefined,
      opts: [Object]
    }
  ],
  state: undefined,
  opts: {
    _exploded: {},
    _verified: {},
    ImportDeclaration: { enter: [Array] },
    Program: { enter: [Array], exit: [Array] },
    JSXAttribute: { enter: [Array] },
    CallExpression: { exit: [Array] },
    ExportDefaultDeclaration: { enter: [Array] },
    FunctionDeclaration: { enter: [Array], exit: [Array] },
    VariableDeclaration: { enter: [Array] },
    ArrowFunctionExpression: { exit: [Array] },
    FunctionExpression: { exit: [Array] },
    JSXNamespacedName: { enter: [Array] },
    JSXSpreadChild: { enter: [Array] },
    JSXElement: { exit: [Array] },
    JSXFragment: { exit: [Array] },
    BlockStatement: { exit: [Array] },
    TSModuleBlock: { exit: [Array] }
  },
  _traverseFlags: 0,
  skipKeys: null,
  parentPath: NodePath {
    contexts: [ [TraversalContext] ],
    state: undefined,
    opts: {
      _exploded: {},
      _verified: {},
      ImportDeclaration: [Object],
      Program: [Object],
      JSXAttribute: [Object],
      CallExpression: [Object],
      ExportDefaultDeclaration: [Object],
      FunctionDeclaration: [Object],
      VariableDeclaration: [Object],
      ArrowFunctionExpression: [Object],
      FunctionExpression: [Object],
      JSXNamespacedName: [Object],
      JSXSpreadChild: [Object],
      JSXElement: [Object],
      JSXFragment: [Object],
      BlockStatement: [Object],
      TSModuleBlock: [Object]
    },
    _traverseFlags: 0,
    skipKeys: null,
    parentPath: NodePath {
      contexts: [Array],
      state: undefined,
      opts: [Object],
      _traverseFlags: 0,
      skipKeys: null,
      parentPath: [NodePath],
      container: [Array],
      listKey: 'body',
      key: 11,
      node: [Node],
      type: 'VariableDeclaration',
      parent: [Node],
      hub: [Object],
      data: null,
      context: [TraversalContext],
      scope: [Scope]
    },
    container: [ [Node] ],
    listKey: 'declarations',
    key: 0,
    node: Node {
      type: 'VariableDeclarator',
      start: 461,
      end: 1356,
      loc: [SourceLocation],
      id: [Node],
      init: [Node]
    },
    type: 'VariableDeclarator',
    parent: Node {
      type: 'VariableDeclaration',
      start: 455,
      end: 1357,
      loc: [SourceLocation],
      declarations: [Array],
      kind: 'const'
    },
    hub: {
      file: [File],
      getCode: [Function: getCode],
      getScope: [Function: getScope],
      addHelper: [Function: bound addHelper],
      buildError: [Function: bound buildCodeFrameError]
    },
    data: null,
    context: TraversalContext {
      queue: [Array],
      priorityQueue: [],
      parentPath: [NodePath],
      scope: [Scope],
      state: undefined,
      opts: [Object]
    },
    scope: Scope {
      uid: 14425,
      path: [NodePath],
      block: [Node],
      labels: Map(0) {},
      inited: true,
      bindings: [Object: null prototype],
      references: [Object: null prototype],
      globals: [Object: null prototype],
      uids: [Object: null prototype],
      data: [Object: null prototype] {},
      crawling: false
    }
  },
  container: Node {
    type: 'VariableDeclarator',
    start: 461,
    end: 1356,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    id: Node {
      type: 'Identifier',
      start: 461,
      end: 493,
      loc: [SourceLocation],
      name: 'LoginPage',
      typeAnnotation: [Node]
    },
    init: Node {
      type: 'ArrowFunctionExpression',
      start: 496,
      end: 1356,
      loc: [SourceLocation],
      id: null,
      generator: false,
      async: false,
      params: [Array],
      body: [Node]
    }
  },
  listKey: undefined,
  key: 'init',
  node: Node {
    type: 'ArrowFunctionExpression',
    start: 496,
    end: 1356,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    id: null,
    generator: false,
    async: false,
    params: [ [Node] ],
    body: Node {
      type: 'BlockStatement',
      start: 515,
      end: 1356,
      loc: [SourceLocation],
      body: [Array],
      directives: []
    }
  },
  type: 'ArrowFunctionExpression',
  parent: Node {
    type: 'VariableDeclarator',
    start: 461,
    end: 1356,
    loc: SourceLocation {
      start: [Position],
      end: [Position],
      filename: undefined,
      identifierName: undefined
    },
    id: Node {
      type: 'Identifier',
      start: 461,
      end: 493,
      loc: [SourceLocation],
      name: 'LoginPage',
      typeAnnotation: [Node]
    },
    init: Node {
      type: 'ArrowFunctionExpression',
      start: 496,
      end: 1356,
      loc: [SourceLocation],
      id: null,
      generator: false,
      async: false,
      params: [Array],
      body: [Node]
    }
  },
  hub: <ref *1> {
    file: File {
      _map: Map(0) {},
      opts: [Object],
      declarations: {},
      path: [NodePath],
      ast: [Node],
      scope: [Scope],
      metadata: {},
      code: "import { useCallback, useState } from 'react';\n" +
        '\n' +
        "import { useSelector } from 'react-redux';\n" +
        "import { Redirect } from 'react-router-dom';\n" +
        "import SwipeableViews from 'react-swipeable-views';\n" +
        '\n' +
        "import styled, { breakpoints, IStyledProp } from '@eduzz/houston-ui/styles/styled';\n" +
        '\n' +
        "import CreateForm from './Create';\n" +
        "import LoginForm from './Form';\n" +
        "import LoginRecoveryAccess from './RecoveryAcces';\n" +
        '\n' +
        "import { selectorIsAuthenticated } from '@/store/selectors';\n" +
        '\n' +
        'const LoginPage: React.FC<IStyledProp> = ({ className }) => {\n' +
        '  const [currentView, setCurrentView] = useState(0);\n' +
        '\n' +
        '  const isAuthenticated = useSelector(selectorIsAuthenticated);\n' +
        '\n' +
        '  const onLogin = useCallback(() => setCurrentView(0), []);\n' +
        '  const onRecoveryAccess = useCallback(() => setCurrentView(2), []);\n' +
        '  const onCreate = useCallback(() => setCurrentView(1), []);\n' +
        '\n' +
        "  if (isAuthenticated) return <Redirect to='/' />;\n" +
        '\n' +
        '  return (\n' +
        '    <div className={className}>\n' +
        "      <SwipeableViews index={currentView} height='100%'>\n" +
        "        <div className='step'>\n" +
        '          <LoginForm onRecoveryAccess={onRecoveryAccess} onCreate={onCreate} />\n' +
        '        </div>\n' +
        "        <div className='step'>\n" +
        '          <CreateForm onCancel={onLogin} />\n' +
        '        </div>\n' +
        "        <div className='step'>\n" +
        '          <LoginRecoveryAccess onCancel={onLogin} onComplete={onLogin} />\n' +
        '        </div>\n' +
        '      </SwipeableViews>\n' +
        '    </div>\n' +
        '  );\n' +
        '};\n' +
        '\n' +
        'export default styled(LoginPage)`\n' +
        '  margin: calc(${({ theme }) => theme.spacing(8)} * -1);\n' +
        '  background-color: red;\n' +
        '\n' +
        "  ${breakpoints.down('sm')} {\n" +
        '    margin: calc(${({ theme }) => theme.spacing(4)} * -1);\n' +
        '  }\n' +
        '\n' +
        '  & .step {\n' +
        '    padding: ${({ theme }) => theme.spacing(8)};\n' +
        '    max-width: 450px;\n' +
        '    margin: auto;\n' +
        '\n' +
        "    ${breakpoints.down('sm')} {\n" +
        '      padding: ${({ theme }) => theme.spacing(4)};\n' +
        '    }\n' +
        '  }\n' +
        '`;\n',
      inputMap: null,
      hub: [Circular *1]
    },
    getCode: [Function: getCode],
    getScope: [Function: getScope],
    addHelper: [Function: bound addHelper],
    buildError: [Function: bound buildCodeFrameError]
  },
  data: null,
  context: TraversalContext {
    queue: [ [Circular *2] ],
    priorityQueue: [],
    parentPath: NodePath {
      contexts: [Array],
      state: undefined,
      opts: [Object],
      _traverseFlags: 0,
      skipKeys: null,
      parentPath: [NodePath],
      container: [Array],
      listKey: 'declarations',
      key: 0,
      node: [Node],
      type: 'VariableDeclarator',
      parent: [Node],
      hub: [Object],
      data: null,
      context: [TraversalContext],
      scope: [Scope]
    },
    scope: Scope {
      uid: 14425,
      path: [NodePath],
      block: [Node],
      labels: Map(0) {},
      inited: true,
      bindings: [Object: null prototype],
      references: [Object: null prototype],
      globals: [Object: null prototype],
      uids: [Object: null prototype],
      data: [Object: null prototype] {},
      crawling: false
    },
    state: undefined,
    opts: {
      _exploded: {},
      _verified: {},
      ImportDeclaration: [Object],
      Program: [Object],
      JSXAttribute: [Object],
      CallExpression: [Object],
      ExportDefaultDeclaration: [Object],
      FunctionDeclaration: [Object],
      VariableDeclaration: [Object],
      ArrowFunctionExpression: [Object],
      FunctionExpression: [Object],
      JSXNamespacedName: [Object],
      JSXSpreadChild: [Object],
      JSXElement: [Object],
      JSXFragment: [Object],
      BlockStatement: [Object],
      TSModuleBlock: [Object]
    }
  },
  scope: Scope {
    uid: 14426,
    path: [Circular *2],
    block: Node {
      type: 'ArrowFunctionExpression',
      start: 496,
      end: 1356,
      loc: [SourceLocation],
      id: null,
      generator: false,
      async: false,
      params: [Array],
      body: [Node]
    },
    labels: Map(0) {},
    inited: true,
    bindings: [Object: null prototype] {
      className: [Binding],
      currentView: [Binding],
      setCurrentView: [Binding],
      isAuthenticated: [Binding],
      onLogin: [Binding],
      onRecoveryAccess: [Binding],
      onCreate: [Binding]
    },
    references: [Object: null prototype] {},
    globals: [Object: null prototype] {},
    uids: [Object: null prototype] {},
    data: [Object: null prototype] {},
    crawling: undefined
  }
}

This is the path output var from the src/components/Pages/Public/Login/index.tsx, as you can see the parentPath.type is VariableDeclarator so it is skip by the if (path.parent.type === 'VariableDeclarator')

@gaearon
Copy link
Collaborator Author

gaearon commented Nov 11, 2021

Can you please clarify by posting the exact minimal snippets that (1) work and (2) don't work?

@danieloprado
Copy link

Sure!

// Does not work
const Component =({ className }) => {
 return <div className={className}>hi!</div>
}

export default styled(Component)`
    background-color: red;
`

// Worked
const Component = styled(({ className }) => {
 return <div className={className}>hi!</div>
})`
    background-color: red;
`

// Worked
const component =({ className }) => { // <~ lowercase
 return <div className={className}>hi!</div>
}

// uppsercase
export default Styled(Component)` 
    background-color: red;
`

@danieloprado
Copy link

danieloprado commented Nov 11, 2021

Here an example: https://stackblitz.com/edit/vitejs-vite-k3bzhu?file=src/Test.jsx

Try to change the background-color or try to change the parameter of the TestMyHoc. TestMyHocWorking is an example where refresh is working as expected, I think is because is not a call assignment.

@gaearon
Copy link
Collaborator Author

gaearon commented Nov 11, 2021

Does the same happen in CodeSandbox? Would be nice to check to see if this is Vite-specific.

@danieloprado
Copy link

It's not happening with react-scritps: https://stackblitz.com/edit/react-ptscnt?file=src%2FTest.jsx
only with Vite: https://stackblitz.com/edit/vitejs-vite-k3bzhu?file=src/Test.jsx

But when use HOC without a call assignment it works.

@gaearon
Copy link
Collaborator Author

gaearon commented Nov 12, 2021

If it's not happening with react-scripts then something is wrong with what Vite is doing so please report there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: MobX-like observer pattern doesn't work with Fast Refresh because Hooks don't get detected
7 participants