-
Notifications
You must be signed in to change notification settings - Fork 45.6k
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
Refactor DOM attribute code #11804
Refactor DOM attribute code #11804
Conversation
31eacd6
to
8669be3
Compare
} | ||
// The rest are treated as attributes with special cases. | ||
const {attributeName, attributeNamespace} = propertyInfo; | ||
if (value === 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.
Can value be undefined
?
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.
No, this would get caught by:
if (shouldTreatAttributeValueAsNull(name, value, isCustomComponentTag)) {
value = null;
}
} | ||
|
||
export function shouldTreatAttributeValueAsNull(name, value) { | ||
if (value === null || typeof value === 'undefined') { |
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 need to do typeof value === 'undefined'
? why not just value === undefined
?
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.
Consistency with all other checks. (I don't care either way but IMO we should change them all together if we want to.)
return prefix !== 'data-' && prefix !== 'aria-'; | ||
} | ||
|
||
export function shouldTreatAttributeValueAsNull(name, value) { |
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 we add type annotations to this function?
@@ -80,30 +68,29 @@ export function createMarkupForRoot() { | |||
* @return {?string} Markup string, or null if the property was invalid. | |||
*/ | |||
export function createMarkupForProperty(name, value) { |
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 we also added type annotations here whilst we're at 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.
Overall, it looks good – the slight performance regression isn't a concern to me. Please, as this is a refactor PR, can we add type annotations to the functions used in this process. I think it will add to the readability and help reduce further bugs if we can. Otherwise, awesome stuff!
@@ -152,84 +127,62 @@ export function getValueForAttribute(node, name, expected) { | |||
* @param {string} name | |||
* @param {*} value | |||
*/ | |||
export function setValueForProperty(node, name, value) { | |||
const propertyInfo = getPropertyInfo(name); | |||
export function setValueForProperty(node, name, value, isCustomComponentTag) { |
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.
Type annotations here too please?
} | ||
if (value === null) { | ||
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.
Could you just return ''
inside of the shouldTreatAttributeValueAsNull
block? When does shouldTreatAttributeValueAsNull
return false but the value is 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.
Nice catch
return false; | ||
} | ||
|
||
export function isBadlyTypedAttributeValue(name, value) { |
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 (isBadlyTypedAttributeValue(name, value)) { | ||
return true; | ||
} | ||
return false; |
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 not just return isBadlyTypedAttributeValue(name, value)
?
8669be3
to
bd30e25
Compare
I noticed some patterns weren't being tested.
The branching before the call is unnecessary because setValueForProperty() already has an internal branch that delegates to deleteValueForProperty() for null and undefined through the shouldIgnoreValue() check. The goal is to start unifying these methods because their separation doesn't reflect the current behavior (e.g. for unknown properties) anymore, and obscures what actually happens with different inputs.
Now we don't read propertyInfo twice in this case. I also dropped a few early returns. I added them a while ago when we had Stack-only tracking of DOM operations, and some operations were being counted twice because of how this code is structured. This isn't a problem anymore (both because we don't track operations, and because I've just inlined this method call).
The special cases for null and undefined already exist in setValueForAttribute().
Their naming is pretty confusing by now. For example setValueForProperty() calls setValueForAttribute() when shouldSetAttribute() is false (!). I want to refactor (as in, inline and then maybe factor it out differently) the relation between them. For now, I'm consolidating the callers to use setValueForProperty().
The naming of these methods is still very vague and conflicting in some cases. Will need further work.
482a300
to
908a807
Compare
This makes the flow clearer in my opinion.
It was previously duplicated. It's also suspiciously similar in purpose to shouldTreatAttributeValueAsNull() so I want to see if there is a way to unify them.
Its naming was confusing and it was used all over the place instead of more specific checks. Now that we only have one call site, we might as well inline and get rid of it.
908a807
to
2484117
Compare
// If the prop is in the special list, treat it as a simple attribute. | ||
if (!propertyInfo) { | ||
if (isAttributeNameSafe(name)) { | ||
const attributeName = 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.
Why alias name
to attributeName
?
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.
To make it extra clear you're not supposed to call setAttribute
with name
unless you know what you're doing.
* @param {DOMElement} node | ||
* @param {string} name | ||
*/ | ||
export function deleteValueForProperty(node, 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.
Can you rename this test since deleteValueForProperty
doesn't exist anymore?
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.
Do we care? There's plenty of internal names, even in test files (e.g. ReactDOMComponent-test
). I agree we need to clean it up by functionality rather than file, but don't think it's necessary to do in 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.
Using names of internal APIs that exist is different. In this case, it's referrencing a function that no longer exists. If I was looking to contribute and encountered that, I would be confused that the function didn't exist anywhere in the codebase.
It's not critical, but since you remove it feels like it's in scope to fix any lingering references. If we're going to restructure the tests soon anyways, it probably doesn't matter.
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 guess what I'm referring to is that there are already plenty examples where internal names don't match tests. It used to be important for them to be in sync when unit test tested internals, but they don't anymore. I agree it's confusing, but IMO mismatches in test file names are even more confusing.
Conceptually those tests are about deleting. I wouldn't move them into set*ForProperty
just because those code paths moved there. Instead I would've preferred to rename all tests to human readable descriptions (e.g. "deletion"). But then it's already inconsistent in the current test codebase so I think it's easier to do in one big cleanup later.
if (shouldTreatAttributeValueAsNull(name, value, isCustomComponentTag)) { | ||
value = null; | ||
} | ||
// If the prop is in the special list, treat it as a simple attribute. |
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.
Did you mean if the prop is not in the special list?
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.
Oops yes
if (mustUseProperty) { | ||
const {propertyName} = propertyInfo; | ||
if (value === null) { | ||
(node: any)[propertyName] = hasBooleanValue ? false : ''; |
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.
All attributes that must use property are boolean values, so technically this check is unnecessary. We could add a new attribute type like HAS_BOOLEAN_PROPERTY
to simplify this, and get rid of the bitmasks again (happy to do in a follow up)
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 follow up. This diff is getting hard to deal with.
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.
Thanks for adding Flow type annotations. LGTM (minus lint error I think?) :)
Ran benchmarks a few times, seems neutral. |
The main goal here is to factor the DOM attribute code in the light of how it currently works, instead of how it was conceived five years ago.
There are quite a few things in the current code that don't make sense anymore. For example, the "property" vs "attribute" distinction in method naming doesn't actually correspond to properties vs attributes. Another example:
shouldSetAttribute
doesn't actually determine whether to set an attribute. These were all results of incremental changes where we tried to tweak the behavior with the least amount of code. But they ended up making the overall flow confusing and adding a lot of special cases.This is an attempt to simplify the code without changing what it does. Individual commits tell how I arrived at that. This mostly changes the logic around setting attributes/properties and doesn't touch the other parts that are still structured the old way (such as
get*Attribute|Property
pairs used for validating SSR). I think we can get to those a bit later.