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

[Fiber] Split dev methods within ReactComponentTreeHook #9096

Merged
merged 13 commits into from
Mar 7, 2017

Conversation

trueadm
Copy link
Contributor

@trueadm trueadm commented Mar 2, 2017

ReactComponentTreeHook was originally intended to be a DEV module but has since had production usage with Fiber. This PR aims on splitting the methods out so that we don't ship extra code in production. Flow annotations were added to places that confused Flow into believing methods were missing on ReactComponentTreeHook.

Note: I'm a bit unsure about the require path remaining as react/lib/ReactComponentTreeHook though @sebmarkbage, do we still need to keep it this way?

@@ -14,16 +14,16 @@

import type { Fiber } from 'ReactFiber';
import type { DebugID } from 'ReactInstanceType';
import type { ComponentTreeHookType } from '../../hooks/ReactComponentTreeHook';
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 this should just say from 'ReactComponentTreeHook'?

}
return info;
},
export type ComponentTreeHookType = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What do you think about two separate types, one for DEV, and another for prod? Then the consumer could cast once to whichever is appropriate depending on whether we're behind DEV or not.

@@ -38,7 +38,9 @@ if (__DEV__) {
if (typeof current === 'number') {
// DebugID from Stack.
const debugID = current;
stack = getStackAddendumByID(debugID);
if (getStackAddendumByID) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd prefer to not do this since we already know we're in DEV. See my comment below on potential way to accomplish this.

Copy link
Contributor

Choose a reason for hiding this comment

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

This check seems unnecessary, other than for flow, since we're within a __DEV__ block. I liked Dan's suggestion below- to use a different type for dev/prod so we can avoid all of the unnecessary not-defined checks below.

var item = getItem(id);
return item ? item.updateCount : 0;
},
ReactComponentTreeHook = Object.assign({}, ReactComponentTreeHook, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: why not call Object.assign() without using return value? Flow?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Flow complains if I do it any other way :)

@@ -108,22 +118,24 @@ if (__DEV__) {
var lifeCycleTimerHasWarned = false;

const clearHistory = function() {
ReactComponentTreeHook.purgeUnmountedComponents();
if (purgeUnmountedComponents) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here, too, it seems odd to add these checks.

Copy link
Collaborator

@sophiebits sophiebits left a comment

Choose a reason for hiding this comment

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

I wonder if we could have an "env: 'dev'" property on the type and then use flow refinements. I think Flow might understand this pattern too:

// in ReactComponentTreeHook
type ProdType = {env: 'prod', foo: F};
type DevType = {env: 'dev', foo: F, bar: B};
var ReactComponentTreeHook : ProdType | DevType = ...;
module.exports = ReactComponentTreeHook;

// elsewhere
const ReactComponentTreeHook = require('ReactComponentTreeHook');
if (__DEV__ && ReactComponentTreeHook.env === 'dev') {
  const ReactComponentTreeHookDev : ComponentTreeHookDevType = ReactComponentTreeHook;
  ...
}

Not entirely sure though. We should probably have this reflectable in some way though to prevent crashes from version mismatches.


const ReactDebugCurrentFrame = {};

if (__DEV__) {
// how do a state that ReactComponentTreeHook is using the ComponentTreeHookDevType type?
const ReactComponentTreeHook: ComponentTreeHookDevType = require('ReactComponentTreeHook');
const ReactComponentTreeHook: ComponentTreeHookDevType = (require('ReactComponentTreeHook'): any);
Copy link
Collaborator

Choose a reason for hiding this comment

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

These look good to me. (@gaearon, I heard from @trueadm that you advised against any but it seems safe in this case.)

@@ -68,7 +69,7 @@ var ReactDebugTool = ((null: any): typeof ReactDebugTool);
if (__DEV__) {
const hooks = [];
const didHookThrowForEvent = {};
const ReactComponentTreeHook = require('react/lib/ReactComponentTreeHook');
const ReactComponentTreeHook: ComponentTreeHookDevType = (require('react/lib/ReactComponentTreeHook'): any);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe this is actually not safe in the case that you use an unminified react-dom.js with react.min.js. Can you check?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't this have failed before if that was the case?

Copy link
Contributor Author

@trueadm trueadm Mar 2, 2017

Choose a reason for hiding this comment

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

Yes this fails: Uncaught TypeError: Cannot read property 'purgeUnmountedComponents' of undefined. What is the strategy for dealing with these issues?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It would not have failed before because we check if (purgeUnmountedComponents), etc, each time.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh wait I'm looking at the wrong commit.

Copy link
Contributor Author

@trueadm trueadm Mar 2, 2017

Choose a reason for hiding this comment

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

That's what I thought, I'm a bit confused as to why they should have been removed if we don't want these errors to show when mixing bundles? I can re-add the safety checks but this will need more testing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@gaearon @bvaughn It seems like we do want to check for each function before calling it so that react-dom.js works with react.min.js, unless you don't think that's valuable.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, I didn't think about this. That's a good observation, you're right.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point, Ben. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@spicyj @bvaughn @gaearon

I've updated the PR with some guards. Let me know if there's something I may have missed? I tested locally with React (min) and ReactDOM (non-min) and it's working, although that's not going to test all the code paths that dev code may go through – so please let me know if something doesn't look right.

@sophiebits
Copy link
Collaborator

Curious, did you see any perf changes from this?

@trueadm
Copy link
Contributor Author

trueadm commented Mar 2, 2017

@spicyj It's really hard to say, the +/- % variance against a ms difference wouldn't be too accurate to report. I'll see if there is any more dev related code that can be stripped tomorrow.

Copy link
Collaborator

@sophiebits sophiebits left a comment

Choose a reason for hiding this comment

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

If we're going to check function existence on every use (seems like a good idea to me), can we make the flow type reflect that by making the properties optional? Otherwise you can forget and won't get a flow error.

Files in isomorphic can actually get away without the checks since I believe they will always be in the same env. It might be nice to keep the checks there too though. In that case you might even only need one type for both dev and prod.

@sebmarkbage
Copy link
Collaborator

Not big fan of checking for existence everywhere. Seems like we can at least wrap it in something like function foo() { if (__DEV__ && doFoo) doFoo(); }. It's noisy and it should statically resolve when we compile with constant __DEV__ flags anyway so it should always be true without runtime checks.

Since we've changed strategies I expect us to break this coupling between isomorphic and renderers soon so that this can be statically resolved safely.

@trueadm
Copy link
Contributor Author

trueadm commented Mar 3, 2017

@sebmarkbage I didn't check everything, I just checked a few known methods that would apply to all other DEV omitted methods. The issue with what you've said is that it still won't work if we mix prod and dev React and ReactDOM packages. That's what I already did, unless I'm missing something you're saying?

@sebmarkbage
Copy link
Collaborator

The prod version would be exporting the foo wrapper as a noop empty function.

@sebmarkbage
Copy link
Collaborator

Here's an example of a pattern we've used in a few places:

var validateDOMNesting = emptyFunction;

@gaearon
Copy link
Collaborator

gaearon commented Mar 3, 2017

This would bloat the production bundle with quite a few long method names.

@trueadm
Copy link
Contributor Author

trueadm commented Mar 3, 2017

@sebmarkbage But this use case is a bit different right? ReactComponentTreeHook.js isn't only used in DEV anymore, the object differs because of runtime needs with dev and prod. Unless you mean we should ship an object with stubbed method names for prod? I think the best solution here is to go ahead with having checks in place, it shouldn't increase bundle size and shouldn't give us runtime errors. Maybe we can re-visit this at a later stage if we have a better overall strategy?

@sebmarkbage
Copy link
Collaborator

Normally DCE locally or with Rollup without exclude these empty functions. The only difference is if they're exposed between two packages. So we should only export the names that are actually shared between packages. That way only those names are bloating the package.

But I don't see why this is needed at all though. The only thing that needs the stack addendum in production is in Fiber. Fiber is only in the renderer. It doesn't rely on any global state.

So why doesn't the relevant code just live in the renderer or in the shared folder that gets copied to both isomorphic and renderer?

@@ -17,17 +17,8 @@ var React = require('React');
var ReactUMDEntry = Object.assign({
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
ReactCurrentOwner: require('react/lib/ReactCurrentOwner'),
ReactComponentTreeHook: require('react/lib/ReactComponentTreeHook'),
Copy link
Collaborator

@sebmarkbage sebmarkbage Mar 3, 2017

Choose a reason for hiding this comment

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

This part is the piece I'm skeptical about.

We only needed this export because the debug ids are shared. We didn't need to export this in prod before Fiber.

Stacks through getStackAddendumByWorkInProgressFiber doesn't use the debug ids. I.e. it doesn't rely on global mutable state and therefore can be completely decoupled.

We only need prod stacks in Fiber through getStackAddendumByWorkInProgressFiber if I understand it correctly.

Therefore, why don't we just put getStackAddendumByWorkInProgressFiber in the renderer so we don't need this connection between the packages in prod?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems to me that if you just split up the ReactComponentTreeHook module into smaller pieces in the shared folder that both ReactComponentTreeHook and the Fiber prod stack thing can depend on the rest will fall naturally from there. ReactComponentTreeHook gets disabled in prod and so the dependencies fall off. Only the small dependencies that you split out (getStackAddendumByWorkInProgressFiber) and its dependencies get embedded in the renderer production package.

…k.js

This moves the non DEV methods to a new file called ReactFiberComponentTreeHook, updates existing references to DEV functions to remain inside DEV flags so they do not pull in ReactComponentTreeHook
@@ -1736,7 +1736,7 @@ describe('ReactComponentTreeHook', () => {
expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
});

describe('stack addenda', () => {
describe.only('stack addenda', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

.only

@@ -122,7 +124,7 @@ function validateExplicitKey(element, parentType) {
'%s%s See https://fb.me/react-warning-keys for more information.%s',
currentComponentErrorInfo,
childOwner,
ReactComponentTreeHook.getCurrentStackAddendum(element)
getCurrentStackAddendum && getCurrentStackAddendum(element)
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 need this check, here and in files below? This method is defined unconditionally, and AFAIK all the callers are also DEV-only files.

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.

This looks fine to me, but let's remove the && checks because they don't seem to be necessary.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

Awesome! So clean.

@trueadm trueadm dismissed sophiebits’s stale review March 7, 2017 13:36

Addressed feedback.

@gaearon
Copy link
Collaborator

gaearon commented Mar 7, 2017

🚢

@trueadm trueadm merged commit 4e9d7ac into facebook:master Mar 7, 2017
@gaearon gaearon deleted the component-tree-hook-dev branch March 20, 2017 16:41
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.

6 participants