Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Refactor of ReactElement React Props equivalence logic #2138

Closed

Conversation

trueadm
Copy link
Contributor

@trueadm trueadm commented Jun 20, 2018

Release notes: none

After studying the issues in #2137, I understood that we currently make some assumptions in terms of the ReactElement equivalence system that are incompatible with what we want from Prepack going forward. This PR aims at fixing those assumptions so they fall more into line with the changes #2137.

This PR makes the following changes:

  • All React equivalence is now handled explicitly in the ReactSet/ReactElementSet/ReactPropsSet to ensure all the logic is located together as one and not sprinkled around the codebase.
  • We use hardModifyReactObjectPropertyBinding to modify final objects that React controls rather than Properties.Set. This function bypasses the normal route of putting the property changes on the effects – thus making it so the property changes are permanent and cannot be reverted. Given the object is immutable, there should never be a case where the property may change anyway – we only change the property as part of the equivalence phase and during reconciliation where the React internals are at play. This is only safe for objects explicitly created by React and marked as final by React – no other object can go through this codepath. Any attempt to use this dangerous function on a non-final object will result in an invariant.
  • Various bug fixes were fixed that were found when Make modifiedProperties map somewhat immutable #2137 was merged and existing tests failed.
  • Removed all the logic around makeNotFinal and use hardModifyReactObjectPropertyBinding instead.

constructor(realm: Realm, equivalenceSet: HashSet<AbstractValue>) {
this.realm = realm;
this.equivalenceSet = equivalenceSet;
export class ReactElementSet extends ReactSet {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does inheritance really help here?

I know we use it for the value taxonomy but I'd prefer to avoid it for the rest of the logic. I imagine you could instantiate ReactSet instead, keep it as a field, and delegate to it when necessary.

Copy link
Contributor Author

@trueadm trueadm Jun 20, 2018

Choose a reason for hiding this comment

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

What's wrong with inheritance? I find this code cleaner and easier to understand/read now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same reason we don't use it in components? :-)

Inheritance makes the dependencies between parts very implicit. It's hard for me to tell what is the contract between ReactSet and its subclasses. Do they call its methods? Does it call their methods? Does both happen? Since all interaction happens through this.something() I have to check every method name to verify where it's defined.

This problem usually becomes even more difficult with time, as superclass become coupled to subclasses, and then one of them needs to behave differently but can't "break out".

Using delegation would make it clearer what ReactSet is doing, and how each of the specific "sets" uses this functionality.

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 have to disagree here, This isn't the same as UI components, this is business logic. I actually find inheritance is fine in cases like this when there's only a single level of inheritance and the contract is explicit.

I'll refactor the classes again though as I don't want to block this PR on arguments of the validity of inheritance :D

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not blocking, I'm just finding it harder to understand what the code is doing because of the implicitness.

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've redone it all and removed the classes, it's all functions and argument passing now. Functional FTW


if (temporalAlias !== undefined) {
currentMap = this._getKey("temporalAlias", currentMap, visitedValues);
result = this._getValue(temporalAlias, currentMap, visitedValues);
Copy link
Contributor

Choose a reason for hiding this comment

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

What case does this handle?

I think this would break if there was a prop called temporalAlias (very unlikely but should be easy to fix).

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's for snapshotting, I've addressed in latest commit :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use a symbol or an object as a key in this case? String clashes with user supplied props (which also use strings).

Copy link
Contributor

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

Overall LGTM.

I found it a bit difficult to understand what methods on ReactSet are "internal" to it and which are meant to be called by subclasses. Would prefer delegation but don't care strongly.

Found a few bugs (#2139) while testing this but they're on master too.

@gaearon
Copy link
Contributor

gaearon commented Jun 20, 2018

Btw I didn't mean to drop classes :-)

I meant that instead of having ReactPropsSet extend ReactSet, you can delegate to it.

class ReactPropsSet {
  reactSet = new ReactSet(whatItActuallyNeeds);

  add() {
    var something = this.reactSet.doSomething();
    // ...
}

Then interaction between them is explicit and I don't need to guess which methods are private to the base class vs which are meant to be accessible by superclass, and whether base class ever calls the superclass.

Anyway, no need to rewrite—hope this explains what I meant.

@gaearon
Copy link
Contributor

gaearon commented Jun 20, 2018

Let's just revert the last commit? The argument count in functions is wild.

I agree classes are better in this case. I'm sorry if my last comments were confusing. I was strictly speaking about dropping inheritance, not dropping classes.

@gaearon
Copy link
Contributor

gaearon commented Jun 20, 2018

Anyway. Whatever you think is clearer 👍 Let's not get stuck on this.

Copy link
Contributor

@hermanventer hermanventer left a comment

Choose a reason for hiding this comment

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

Looks good to me.

@@ -91,6 +91,20 @@ export function isReactElement(val: Value): boolean {
return false;
}

export function isReactProps(val: Value): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps call this "inReactProps"? Come to think of it, reactProps is not a very helpful name for someone unfamiliar with the code base.

Copy link
Contributor

@gaearon gaearon Jun 20, 2018

Choose a reason for hiding this comment

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

Maybe isReactPropsObject? A single object is called "props", it's not a plural in this sense.

The fact that it checks a set is an implementation detail. It's meant to be more of a brand check conceptually. Like isArray rather than inArrays or isArrays.

Copy link

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

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

@hermanventer is landing this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@@ -1153,7 +1153,18 @@ export function hardModifyReactObjectPropertyBinding(
"hardModifyReactObjectPropertyBinding can only be used on final objects!"
);
let binding = object.properties.get(propName);
invariant(binding !== undefined);
if (binding === undefined) {
binding = {
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like we're trying to create an object to match the shape below. Can we instead make peace with it being undefined, and remove the invariant below? Object.assign should work either way.

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 it's undefined, then we need to create the binding. That's why we do it here.

Copy link

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

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

@trueadm is landing this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

facebook-github-bot pushed a commit that referenced this pull request Jun 21, 2018
Summary:
Release notes: none

This PR contains some fixes and small tidy ups that would have been in #2138. There was a bug to do with the React props equivalence system and snapshots that was found when #2137 was merged (along with the fix I commented to on that PR). This PR unblocks #2137 to land as it should now pass our internal bundle test.
Closes #2146

Differential Revision: D8556958

Pulled By: trueadm

fbshipit-source-id: dd774de76059e00af1dff305dd3618c3bfed06ea
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants