-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Fiber test renderer #8628
Fiber test renderer #8628
Conversation
👍 I think we'll want to keep the tests for the stack implementation of |
|
||
var instanceCounter = 0; | ||
|
||
var ReactTestFiberComponent = { |
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.
Note: the separation between ReactDOMFiber
and ReactDOMFiberComponent
is just an artifact of how it was forked from ReactDOMComponent
and that we still want to keep them in sync when possible. So feel free to abandon that convention (e.g. see ReactNoop.js
)
It would be fine to branch on renderer in the test file like the ReactART tests do. |
Thanks for the feedback thus far. Regarding refs I think I can reuse the class Component extends React.Component {
render() {
return (
<div className="purple">
<div />
<Child />
</div>
);
}
}
class Child extends React.Component {
render() {
return (
<div className="green" />
);
}
}
ReactTestRenderer.create(<Component />);
const hostInstance = TestRenderer.findHostInstance(root);
/* hostInstance ==>
{ type: 'div',
children:
[ { '$$typeof': Symbol(react.element),
type: 'div',
key: null,
ref: null,
props: {},
_owner: [Object],
_store: {} },
{ '$$typeof': Symbol(react.element),
type: [Function: Child],
key: null,
ref: null,
props: {},
_owner: [Object],
_store: {} },
{ type: 'div', children: null, props: {} },
{ type: 'div', children: null, props: [Object] } ],
props: { className: 'purple' } }
*/ Is that expected? Why? |
Second issue is if I try to call class Component extends React.Component {
render() {
return (
<div className="purple">
<div />
{/* this callback ref will throw */}
<Child ref={fiber => console.log(ReactTestRenderer.findHostInstance(fiber)} />
</div>
);
}
}
class Child extends React.Component {
render() {
return (
<div className="green" />
);
}
}
ReactTestRenderer.create(<Component />);
// ref calback throws:
// Invariant Violation: Unable to find node on an unmounted component. I would expect that to be mounted by the time the ref callback is called... |
Actually, ignore everything I’ve said. I think I see a path forward. |
705386f
to
d143839
Compare
Woohoo! Only 2 failing tests now! I have ReactTestRenderer-test branching on the ReactDOMFeatureFlags useFiber switch now. To support the With the two failing tests, I am curious if these are failing because the feature is incomplete or because behavior has changed subtly.
Is this warning still to be implemented? If yes I would suspect it to “just work” here without any additional effort.
The ref callback is not being called yet on unmount. This is another situation where I would expect this to just work. Is this not yet implemented or do I need to track down a bug in this renderer?
The order of ilfecycles is different here. Based on watching the error boundary work between @acdlite and @gaearon I suspect that this may be expected. Before
After
|
Just checked master. It looks like |
@iamdustan Want to help with implementing that warning in master as a separate PR? |
@@ -86,7 +86,11 @@ module.exports = function<T, P, I, TI, C, CX>( | |||
function attachRef(current : ?Fiber, finishedWork : Fiber, instance : any) { | |||
const ref = finishedWork.ref; | |||
if (ref && (!current || current.ref !== ref)) { | |||
ref(instance); | |||
if (typeof config.getPublicInstance === 'function') { |
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.
Please destructure this from config earlier. We don't want to read that value every time. I think we could also make this required to keep contract strict.
parentHostContext : HostContext, | ||
type : string, | ||
) : HostContext { | ||
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.
Please return the same object (emptyObject like in ReactNoop) so it doesn't push stack unnecessarily.
11f3b34
to
6bc4167
Compare
Can you please update this now that the SFC warning is in? |
595a489
to
a63c10a
Compare
The only failing test remaining is that it The test expects the ref to be called with My biggest concern is that being called twice wasn’t necessarily an intentional decision before, but the test was added to assert the behavior and not regress accidentally. I’m not certain how much effort should be put into maintaining this because I think it would require changing @spicyj what thoughts or advise do you have since you wrote the original implementation? |
parentInstance.removeChild(child); | ||
}, | ||
|
||
scheduleAnimationCallback: window.requestAnimationFrame, |
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 this will throw in non-jsdom environment. Can you make it setTimeout
? It seems like it wouldn't be used anyway, but at least we shouldn't throw.
rootContainerInstance : Container, | ||
) : boolean { | ||
// console.log('finalizeInitialChildren'); | ||
// setInitialProperties(testElement, type, props, rootContainerInstance); |
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.
Please clean up the comments.
if (typeof child.toJSON === 'function') { | ||
childrenJSON.push(child.toJSON()); | ||
} else if (typeof child.text !== 'undefined') { | ||
childrenJSON.push(); |
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.
Are pushing undefined
? Why?
} | ||
|
||
toJSON() { | ||
// eslint-disable ignore the children |
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.
What are we ignoring specifically? Generally I'd like to avoid adding eslint ignores.
} | ||
|
||
update(type, props) { | ||
// console.log('update', type, props, this.children); |
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.
Comments
text : text, | ||
id: instanceCounter++, | ||
rootContainerInstance, | ||
toJSON: () => isNaN(+inst.text) ? inst.text : +inst.text, |
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 not sure this is right. For example even "2"
would become 2
. Maybe this is unobservable implementation detail and we should just always use strings? Since that's what they get coerced to anyway.
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.
good point. this was to make one of the tests pass, but I think to be correct I need to capture the type when creating the TextInstance and only coerce back to a number if it came in as a number.
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 think you'd actually get a number though in createTextInstance
? As far as I can see it's already coerced by Fiber code by then. I think it actually makes more sense than exposing this to the renderer, and I'm fine with divergent behavior here (always treating text as strings).
@@ -317,6 +343,7 @@ describe('ReactTestRenderer', () => { | |||
]); | |||
}); | |||
|
|||
// this is only passing in Fiber because refs aren't actually working |
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.
Is this comment outdated? It’s not telling me much: are refs broken in Fiber? Are they broken specifically for test renderer? What is our plan to fix them? Is this within the scope of this PR?
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.
outdated comment.
createNodeMock: Function; | ||
|
||
constructor(rootID, createNodeMock) { | ||
this.rootID = rootID; |
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 see rootID
being used anywhere. Is it needed?
} | ||
var container = new TestContainer('<default>', createNodeMock); | ||
var root = TestRenderer.createContainer(container); | ||
if (root) { |
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.
When is it 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 don’t think ever, but flow was complaining that TestRenderer.createContainer
may return 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.
You'd need to see why other renderers didn't need this. It probably inferred that OpaqueNode
type can be null, maybe from some other method definition.
}, | ||
|
||
/* eslint-disable camelcase */ | ||
unstable_batchedUpdates() { |
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.
Can you just omit it? I don't think we want to expose it here.
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 yeah, forgot about this one. The stack-based ReactTestRenderer exposes this currently..
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.
Exposing but throwing isn't much better. :-)
Let's either make it work or remove it.
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.
😇
...adding this to the PR checklist.
} | ||
ReactDOM.render(<Foo useDiv={true} />, container); | ||
ReactDOM.render(<Foo useDiv={false} />, container); | ||
expect(log).toEqual(['div', null, 'span']); |
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.
Ideally also unmount at the end and check we get another 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.
oh whoops, didn’t mean to actually commit this. This was testing the behavior difference for refs between the test-renderer and DOM renderer re #8628 (comment)
I think the existing test for updates might be checking accidental implementation details. It's not testing refs themselves, it's testing calls of In particular, I don't see why it calls |
Right now We can try caching the mock node on the instance and only calling |
@@ -463,7 +463,7 @@ module.exports = function<T, P, I, TI, C, CX, CI>( | |||
} | |||
const ref = finishedWork.ref; | |||
if (ref) { | |||
const instance = getPublicInstance(finishedWork.stateNode); | |||
const instance = (getPublicInstance(finishedWork.stateNode) : any); |
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.
the flow type for instance
on master is any
. Whilst adding the PI
parameter does end up saying instance
is PI
, that fails flow due to ref expecting:
ref: null | (((handle : ?Object) => void) & { _stringRef: ?string }),
I suspect this could be improved, but for time constraints at the moment I simply dropped it back to equal the current inference.
|
||
constructor(element: ReactElement) { | ||
this._currentElement = element; | ||
this._renderedChildren = null; | ||
this._topLevelWrapper = null; | ||
this._hostContainerInfo = null; | ||
this._nodeMock = UNSET; |
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.
Could we just assign it here instead? We already know the element
.
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, would this need to be invalidated on receiveComponent
?
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.
Hmm, that's trickier. Probably not since nodes are supposed to be kept intact. I'm actually not sure the API is right now, maybe we should've just passed the type
instead of the element
. But what's done is done?
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.
welp, I’ve updated it to on mountComponent
because that is the earliest point we can do so (that is when hostContainerInfo
is passed in)
Ugh, GH collapsed my comment. Here it is: #8628 (comment). |
First two thoughts on how to fix this are:
|
@@ -28,7 +32,8 @@ type ReactTestRendererJSON = {| | |||
|
|||
type Container = {| | |||
children: Array<Instance | TextInstance>, | |||
createNodeMock: Function | |||
createNodeMock: Function, | |||
$$typeof: typeof CONTAINER_TYPE, |
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 this could be just $$typeof: CONTAINER_TYPE
?
Same below.
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.
without the typeof
I get the following:
src/renderers/testing/ReactTestRendererFiber.js:36
36: $$typeof: CONTAINER_TYPE,
^^^^^^^^^^^^^^ string. Ineligible value used in/as type annotation (did you forget 'typeof'?)
36: $$typeof: CONTAINER_TYPE,
^^^^^^^^^^^^^^ CONTAINER_TYPE
This looks good to me. Thanks for sticking with it. 😄 |
799e901
to
464b273
Compare
bad531c
to
32991b8
Compare
) : void { | ||
const index = parentInstance.children.indexOf(child); | ||
if (index !== -1) { | ||
this.children.splice(index, 1); |
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.
What is this.children
?
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.
a bug. should be parentInstance.children
.
Congratulations. 😄 |
https://github.com/facebook/react/blob/master/src/renderers/shared/fiber/ReactFiberReconciler.js#L173 Should be updated to return |
@@ -462,7 +463,7 @@ module.exports = function<T, P, I, TI, C, CX>( | |||
} | |||
const ref = finishedWork.ref; | |||
if (ref) { | |||
const instance = finishedWork.stateNode; | |||
const instance = getPublicInstance(finishedWork.stateNode); |
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 not type safe for the case finishedWork.tag === ClassComponent
.
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.
how so? what can I do to rectify that?
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.
finishedWork.stateNode
on a a class component will be the instance of the class. That doesn't match the generic types I | TI
. I think it only accidentally works on runtime now because you assume they won't have a .tag
field with the string INSTANCE
so they fallback.
You can make separate branches by switching on finishedWork.tag
and only use getPublicInstance
on the branch on HostComponent
.
|
||
useSyncScheduling: true, | ||
|
||
getPublicInstance(inst) { |
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.
Hm. It's strange that you're allowed to avoid the type annotation here. It probably leads to some overly optimistic analysis by Flow. We should add a type annotation here. It's important to do in the renderers since otherwise the generic arguments can be inferred to be very weak.
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.
Related, how can I type the createNodeMock
function so PI
is properly inferred?
See: facebook/react#8628 (comment) These are used for the internal context stack. See https://github.com/facebook/react/blob/2da35fcae888752948454dd04e9573cc7bdba8f5/src/renderers/shared/fiber/ReactFiberHostContext.js#L84-L102
* Fix ReactFiberReconciler annotation to include `PI` #8628 (comment) * Make getPublicInstance type safe * attempting to trigger the issue @sebmarkbage described #8751 (comment) * Unify renderer-specific getPublicInstance with getRootInstance * Switch on fiber type HostComponent for getPublicRootInstance * Fix test that was too dynamic (and failing) * Use PI in reconciler public API * Prettier
* ReactTestRenderer move current impl to stack dir * ReactTestRenderer on fiber: commence! * ReactTestRenderer: most non-ref/non-public-instance tests are passing * Move ReactTestFiberComponent functions from Renderer to Component file * test renderer: get rid of private root containers and root Maps * TestRenderer: switch impl based on ReactDOMFeatureFlag.useFiber * ReactTestRenderer: inline component creation * ReactTestRenderer: return to pristine original glory (+ Fiber for error order difference) * TestRendererFiber: use a simple class as TestComponentInstances * Add `getPublicInstance` to support TestRenderer `createNodeMock` * Rename files to end. Update for `mountContainer->createContainer` change * test renderer return same object to prevent unnecessary context pushing/popping * Fiber HostConfig add getPublicInstance. This should be the identity fn everywhere except the test renderer * appease flow * Initial cleanup from sleepy work * unstable_batchedUpdates * Stack test renderer: cache nodeMock to not call on unmount * add public instance type parameter to the reconciler * test renderer: set _nodeMock when mounted * More cleanup * Add test cases for root fragments and (maybe?) root text nodes * Fix the npm package build Explicitly require the Stack version by default. Add a separate entry point for Fiber. We don't add fiber.js to the package yet since it's considered internal until React 16. * Relax the ref type from Object to mixed This seems like the most straightforward way to support getPublicInstance for test renderer. * Remove accidental newline * test renderer: unify TestComponent and TestContainer, handle root updates * Remove string/number serialization attempts since Fiber ensures all textInstances are strings * Return full fragments in toJSON * Test Renderer remove TestComponent instances for simple objects * Update babylon for exact object type syntax * Use $$typeof because clarity > punching ducks. * Minor Flow annotation tweaks * Tweak style, types, and naming * Fix typo
This is the start of reimplementing
ReactTestRenderer
on Fiber. Current tests status:unstable_batchedUpdates
A good chunk of those tests have to do with refs and the ability to provide a
createNodeMock
function. I believe thatReactFiberReconciler
will need to be updated with agetPublicInstance
method to handle this.Ignore all changes in
ReactTestRenderer-test.js
. The first is to import the fiber renderer instead of stack (actually, is there a better way to do this? should I copy the ReactTestRenderer-test for ReactTestFiberRenderer?), the rest are testing/skip/etc and will be backed out when done.cc @spicyj