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

Warn for callback refs on functional components (Stack + Fiber) #8635

Merged
merged 14 commits into from
Jan 9, 2017

Conversation

iamdustan
Copy link
Contributor

@iamdustan iamdustan commented Dec 24, 2016

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:

  • Are there any areas of the fiber impl where errors and warnings are expected to live? This is the first instance added to BeginWork which I think may indicate this warning should exist elsewhere
  • This doesn’t actually stop any ref from being attached. How should that be done?
  • A related test seems to show refs throwing an error if used on a top-level SFC. Should that exist in the same area of the code or elsewhere?

warning(
!!workInProgress.ref,
'Stateless function components cannot be given refs ' +
'(See ref "' + workInProgress.ref._stringRef + '" in ' +
Copy link
Collaborator

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?

Copy link
Contributor Author

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)

Copy link
Collaborator

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.

@gaearon
Copy link
Collaborator

gaearon commented Dec 24, 2016

This is the first instance added to BeginWork which I think may indicate this warning should exist elsewhere

This looks fine to me. It’s just because we don’t have all warnings yet. A bunch of class-related warnings are in ClassComponent but those functions are called from begin phase as well.

This doesn’t actually stop any ref from being attached. How should that be done?

Is behavior different from Stack? AFAIK it also attaches a ref, just calls it with null.

A related test seems to show refs throwing an error if used on a top-level SFC. Should that exist in the same area of the code or elsewhere?

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.

@gaearon
Copy link
Collaborator

gaearon commented Dec 24, 2016

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) + '). ' +
Copy link
Collaborator

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).

Copy link
Contributor Author

@iamdustan iamdustan Dec 24, 2016

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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yep.

Copy link
Collaborator

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.

Copy link
Contributor Author

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?

@iamdustan
Copy link
Contributor Author

@gaearon
Copy link
Collaborator

gaearon commented Dec 24, 2016

This is the throw test that still isn't passing I was referring to.
https://github.com/facebook/react/blob/master/src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js#L137

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.

@iamdustan
Copy link
Contributor Author

Ah. Gotcha. Totally misunderstood that test. 👍

@iamdustan
Copy link
Contributor Author

Alright, updated accordingly.

Commits:

  • first commit updates stack reconciler to use stack in warning.
  • add support for fiber warning on string refs in the same way
  • warn for function refs in stack and fiber
  • run the record-tests script

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.

}

warning(
!workInProgress.ref,
Copy link
Collaborator

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.

Copy link
Contributor Author

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');
Copy link
Collaborator

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.

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 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)
);
Copy link
Collaborator

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 **)'
Copy link
Collaborator

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?

Copy link
Collaborator

@gaearon gaearon left a 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)
Copy link
Collaborator

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)
Copy link
Collaborator

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.

@gaearon
Copy link
Collaborator

gaearon commented Dec 27, 2016

Actual review here: #8635 (review). I don't see why GH puts outdated comments after it.

@iamdustan iamdustan force-pushed the statelss-component branch 2 times, most recently from b87524f to a42306a Compare December 28, 2016 15:34
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' +
Copy link
Contributor Author

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 FunctionalComponents then I get Foo...

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’m going to push one more commit with that change for review since it looks like you landed that getCurrentFiberOwnerName change yesterday.

@iamdustan iamdustan force-pushed the statelss-component branch 3 times, most recently from dd54156 to 21aee21 Compare December 28, 2016 16:08
case ClassComponent:
return getComponentName(fiber);
case FunctionalComponent:
Copy link
Contributor Author

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 />.

Copy link
Collaborator

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.

Copy link
Contributor Author

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 FunctionalComopnents their own case.

case FunctionalComponent:
if (fiber._debugOwner != null) {
return getComponentName(fiber._debugOwner);
}
Copy link
Collaborator

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.

Copy link
Contributor Author

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 FunctionalComponents name in the same location that stack logs the FunctionalComponents owners name.

Copy link
Collaborator

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.

@iamdustan
Copy link
Contributor Author

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.

@gaearon
Copy link
Collaborator

gaearon commented Jan 9, 2017

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’m trying to figure out the proper fix and want to get it merged today.

@gaearon gaearon force-pushed the statelss-component branch from 6b898d2 to 1367f6e Compare January 9, 2017 19:50
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.
@gaearon gaearon force-pushed the statelss-component branch from 1367f6e to fafb8ef Compare January 9, 2017 20:38
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.
@gaearon gaearon force-pushed the statelss-component branch from fafb8ef to a070e9a Compare January 9, 2017 21:07
@gaearon gaearon changed the title Fiber: warn for refs on SFCs Warn for callback refs on functional components (Stack + Fiber) Jan 9, 2017
@gaearon gaearon added this to the 15-lopri milestone Jan 9, 2017
return null;
default:
return null;
if (fiber._debugOwner != null) {
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 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 {
Copy link
Contributor Author

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?

Copy link
Collaborator

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.

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 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?

Copy link
Collaborator

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.

@gaearon
Copy link
Collaborator

gaearon commented Jan 9, 2017

Okay, let's wait for CI on this and then merge.
Thanks for working on this!

@iamdustan
Copy link
Contributor Author

...and thanks for finishing it!

@gaearon gaearon merged commit 90294ea into facebook:master Jan 9, 2017
@iamdustan iamdustan deleted the statelss-component branch January 10, 2017 16:42
@gaearon
Copy link
Collaborator

gaearon commented Jan 10, 2017

Re-tagged as minor since it's technically a new warning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants