-
Notifications
You must be signed in to change notification settings - Fork 47.4k
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
Warn for callback refs on functional components (Stack + Fiber) #8635
Conversation
warning( | ||
!!workInProgress.ref, | ||
'Stateless function components cannot be given refs ' + | ||
'(See ref "' + workInProgress.ref._stringRef + '" in ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fails for non-string refs, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I opened this early for feedback (thanks, btw!)
I didn't see a test case for what should happen in the function ref case already. assuming there isn't a current message for that case what should it be? I don't think it will be too bad to add that case (I'm fiber at least. Will need to look into amending stack)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no test case because we silently give you null
. It's bad though.
I'd prefer we keep warning parity for now. So if we add it to Fiber we should also backport it to Stack. See #8635 (comment) for context.
This looks fine to me. It’s just because we don’t have all warnings yet. A bunch of class-related warnings are in
Is behavior different from Stack? AFAIK it also attaches a ref, just calls it with
Which test are you referring to? I think it would be easier to discuss once some tests start passing. We can always change where we emit the warnings but we don’t want to choose before we cover the actual use cases. |
Related: it would be great to warn for ref callbacks too. They don't work for functional components either. However even Stack doesn't warn about this. Maybe this is out of scope of this PR though, but up to you. Previous attempt was in #7272. |
'Stateless function components cannot be given refs ' + | ||
'(See ref "' + workInProgress.ref._stringRef + '" in ' + | ||
getComponentName(workInProgress) + ' created by ' + | ||
getComponentName(workInProgress.return) + '). ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wrong I think. return
will be parent, not owner. For example in <div><Foo /></div>
, Foo
's return
is div
.
I think you should use ReactDebugCurrentFiber.getCurrentFiberOwnerName()
instead. And in fact maybe you can use ReactDebugCurrentFiber.getCurrentFiberStackAddendum()
to add a nice stack instead of "see ref". You could change this in Stack too by using ReactComponentTreeHook.getStackAddendumByID(this._debugID)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect!
To be clear, the tasks are:
- update the current test case to have the stack addendum rather than just the owner name
- update both stack and fiber impls for that behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's change https://github.com/facebook/react/blob/master/src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js#L149 to put component in a div
to make sure we're printing owner rather than parent name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems as though ReactDebugCurrentFiber.getCurrentFiberStackAddendum()
and ReactComponentTreeHook.getStackAddendumByID(this._debugID)
return slightly different stacks.
Fiber:
"Warning: Stateless function components cannot be given refs. Attempts to access this ref will fail.
in StatelessComponent (at ReactStatelessComponent-test.js:156)
in Parent (at ReactStatelessComponent-test.js:160)"
Stack
"Warning: Stateless function components cannot be given refs Attempts to access this ref will fail.
in Parent (at ReactStatelessComponent-test.js:160)"
This is with a render method of:
render() {
return <div><StatelessComponent name="A" ref="stateless"/></div>;
}
Is this acceptable deviance? If yes should I just make the test loose enough to assert on the commonalities? If not how do you want me to proceed?
This PR gets the following test to pass: This is the throw test that still isn't passing I was referring to. |
This is a test for a different unrelated case. It says that you can't use string refs inside SFCs. What you're adding support for is that you can't use string refs on SFCs. |
Ah. Gotcha. Totally misunderstood that test. 👍 |
e1428ad
to
5c8a52c
Compare
Alright, updated accordingly. Commits:
I had to update ReactIncrementalSideEffects-test.js to swallow the new warning to keep the test from failing. Behavior is the same in that. To support warning for the callback ref in stack I pulled the CompositeTypes enum out of ReactCompositeComponent.js to share between that file and the attach ref file. |
5c8a52c
to
0d3858e
Compare
0d3858e
to
51a1ee0
Compare
} | ||
|
||
warning( | ||
!workInProgress.ref, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One unfortunate thing related to our warning
transformation is that getCurrentFiberOwnerName()
runs regardless of this condition. Since this is a hot path in dev, can you please hide it behind a check?
if (workInProgress.ref != null) {
...
warning(false, ...)
}
Yes, this code is funny.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
haha. I’ve seen this type of thing a few times. Now I understand why. :)
@@ -1069,7 +1069,7 @@ describe('ReactIncrementalSideEffects', () => { | |||
}); | |||
|
|||
it('invokes ref callbacks after insertion/update/unmount', () => { | |||
|
|||
spyOn(console, 'error'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the warning wasn't intentional in this test we should fix the reason it occurs. If it was intentional we should assert on the console.error
message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the warning is expected. There is a stateless component in this test that should fire a warning. I added the same warning spy that is elsewhere to this test.
'Attempts to access this ref will fail.%s%s', | ||
info, | ||
ReactComponentTreeHook.getStackAddendumByID((owner || component)._debugID) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have almost identical code here and in CompositeComponent? Why can't it live in one place?
'Warning: Stateless function components cannot be given refs. ' + | ||
'Attempts to access this ref will fail. Check the render method ' + | ||
'of `Parent`.\n' + | ||
' in Parent (at **)' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there not enough information? Do you have a theory explaining this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite understand why the warning is in two places but you should be able to consolidate the occurrences.
The string ref code path is ReactRef.attachRef
-> ReactOwner.addComponentAsRefTo
-> ReactCompositeComponent.attachRef
.
The callback ref code path is ReactRef.attachRef
(no middlemen).
So it seems like ReactRef.attachRef
runs in both cases, and is a better place to put the warning. You just need to lift it from the ref type condition so that it covers both cases.
Once you start using component._debugID
there, you should get the full stack. Then you can remove the branches from the warning checks.
Finally please either fix the test in Incremental or add an expectation for which exactly warning should fire there (if it's justified) so that we know if we accidentally regress it and it causes a different warning.
'Stateless function components cannot be given refs. Attempts to ' + | ||
'access this ref will fail.%s%s', | ||
info, | ||
ReactComponentTreeHook.getStackAddendumByID(this._debugID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you want component._debugID
, not this._debugID
here. Because you want the stack for the child component (the one whose ref we're trying to attach) rather than the owner (the one who's going to own the ref).
'Stateless function components cannot be given refs. ' + | ||
'Attempts to access this ref will fail.%s%s', | ||
info, | ||
ReactComponentTreeHook.getStackAddendumByID((owner || component)._debugID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly you want the component
's debugID
, not the owner
's. I think component
is guaranteed to exist since we're calling component.getPublicInstance()
on the next line.
Actual review here: #8635 (review). I don't see why GH puts outdated comments after it. |
b87524f
to
a42306a
Compare
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( | ||
'Warning: Stateless function components cannot be given refs. ' + | ||
'Attempts to access this ref will fail. Check the render method ' + | ||
'of `FunctionalComponent`.\n' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the thorough review, @gaearon. This is my own remaining question. While this test passes this looks wrong to me. (The same set of assertions fails in ReactStatelessComponent with fiber on).
I would expect the ownerName
to be Foo
rather than FunctionalComponent
. If I cahnge the getCurrentFiberOwnerName()
to return the fiber._debugOwner
for FunctionalComponent
s then I get Foo
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m going to push one more commit with that change for review since it looks like you landed that getCurrentFiberOwnerName
change yesterday.
dd54156
to
21aee21
Compare
case ClassComponent: | ||
return getComponentName(fiber); | ||
case FunctionalComponent: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I put FunctionalComponent
directly with HostComponent
where it returns null
when there is no fiber._debugOwner
then three following tests move from the fiber success to fiber failure:
src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
-* catches reconciler errors in a boundary during mounting
-* catches reconciler errors in a boundary during update
src/renderers/shared/shared/__tests__/ReactComponent-test.js
-* includes owner name in the error about badly-typed elements
All three of those tests specifically; include a FunctionalComponent
returning const X = undefined; return <X />
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would you move them together with HostComponent
? I'm not sure I'm following.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, idk. It was just a thing I tried and wanted to document. I tried it in an attempt to not give FunctionalComopnent
s their own case.
case FunctionalComponent: | ||
if (fiber._debugOwner != null) { | ||
return getComponentName(fiber._debugOwner); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't look right to me. There's nothing special about functional components from the warning message perspective as far as I know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I remove this line then the following three tests fail:
src/renderers/shared/fiber/__tests__/React
IncrementalSideEffects-test.js
-* invokes ref callbacks after insertion/update/unmount
src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
* should update stateless component
* should unmount stateless component
* should pass context thru stateless component
-* should warn when given a string ref
-* should warn when given a function ref
They all fail for the same reason. This is an example of the results from the current stack reconciler.
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
'Warning: Stateless function components cannot be given refs. ' +
'Attempts to access this ref will fail. Check the render method ' +
'of `Foo`.\n' +
' in FunctionalComponent (at **)\n' +
' in div (at **)\n' +
' in Foo (at **)'
);
If I remove this line then Fiber ends up with:
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
'Warning: Stateless function components cannot be given refs. ' +
'Attempts to access this ref will fail. Check the render method ' +
~ 'of `FunctionalComponent`.\n' +
' in FunctionalComponent (at **)\n' +
' in div (at **)\n' +
' in Foo (at **)'
);
tldr; without this line Fiber ends up logging the FunctionalComponent
s name in the same location that stack logs the FunctionalComponent
s owners name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but this doesn’t mean functional components are special compared to class components, does it? I think this might mean we need separate methods, e.g. getCurrentFiberOwnerName
and getCurrentFiberName
. Or something like this. Maybe we’re wrong in using getCurrentFiberOwnerName
for both of these errors, and one of them needs something different.
I haven’t really thought about this much, just giving you ideas to think about. It’s more likely that these errors need different Fibers in messages, than that functional components are special.
Hey @gaearon, just got back from being out of town on holiday last week. I see you pushed a few commits to this. Where does this stand currently? Want me to pick it back up and explore your last comments a bit further or were you intending on continuing to push it? I have a lot of work to catch up on early this week, but will pick it back up later this week unless you say otherwise. |
I’m trying to figure out the proper fix and want to get it merged today. |
6b898d2
to
1367f6e
Compare
We don't have a test for it. It's easy to mess it up and print the wrong thing so instead of verifying it I'll just remove this bit.
This happened to work, but it is just a coincidence. This change didn’t really match what the function was supposed to be doing. I’m not sure what the correct fix would be yet so this commit breaks tests.
This passes in Stack. It helps ensure we supply the correct owner name.
1367f6e
to
fafb8ef
Compare
This brings the Fiber behavior in line with how Stack does it. The Fiber test was passing accidentally but broke down in more complicated cases (such as when we have an <Indirection> between the owner and the failing element). Now we can also remove the weird cases from getCurrentFiberOwnerName() that didn't really make sense but helped get the (incomplete) tests pass in the past.
fafb8ef
to
a070e9a
Compare
return null; | ||
default: | ||
return null; | ||
if (fiber._debugOwner != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I suspected this was going to be reverted from your last comments.
@@ -328,7 +333,7 @@ exports.createFiberFromText = function(content : string, priorityLevel : Priorit | |||
return fiber; | |||
}; | |||
|
|||
function createFiberFromElementType(type : mixed, key : null | string) : Fiber { | |||
function createFiberFromElementType(type : mixed, key : null | string, debugOwner : null | Fiber | ReactInstance) : Fiber { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so the resulting fix is passing the debugOwner
through the createFiberFrom*
functions explicitly. Yeah?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn’t find a better solution.
Using the "current fiber" was wrong here because the thing being created is the current fiber, but it's not created yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that makes sense. The two ideas i was going to explore was to either create something like getDebugFiberWorkInProgress
or floundering my way through something like what you did.
What were the other solutions you tried that didn’t work out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't really try anything else.
create something like getDebugFiberWorkInProgress
The problem here is that the fiber isn't created yet so you wouldn't be able to set a reference.
Okay, let's wait for CI on this and then merge. |
...and thanks for finishing it! |
Re-tagged as minor since it's technically a new warning. |
For the changelog, this also implements #7267 in Stack.
It implements it for Fiber too, in addition to other Stack-related Fiber warnings and invariants.
==========
While working on the fiber implementation of ReactTestRenderer I came across this warning which is not yet implemented for fiber.
Initial questions: