mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Warn about conflicting style values during updates (#14181)
This is one of the most insidious quirks of React DOM that people run into. Now we warn when we think an update is dangerous.
We still allow rendering `{background, backgroundSize}` with unchanging values, for example. But once you remove either one or change `background` (without changing `backgroundSize` at the same time), that's bad news. So we warn.
Fixes #6348.
This commit is contained in:
1 parent
d5e1bf07d0
commit
8ae867e6b5
4 files changed
+492
No files matched your search
@@ -146,6 +146,141 @@ describe('ReactDOMComponent', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should warn for conflicting CSS shorthand updates', () => {
|
||||
const container = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
<div style={{font: 'foo', fontStyle: 'bar'}} />,
|
||||
container,
|
||||
);
|
||||
expect(() =>
|
||||
ReactDOM.render(<div style={{font: 'foo'}} />, container),
|
||||
).toWarnDev(
|
||||
'Warning: Removing a style property during rerender (fontStyle) ' +
|
||||
'when a conflicting property is set (font) can lead to styling ' +
|
||||
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
|
||||
'properties for the same value; instead, replace the shorthand ' +
|
||||
'with separate values.' +
|
||||
'\n in div (at **)',
|
||||
);
|
||||
|
||||
// These updates are OK and don't warn:
|
||||
ReactDOM.render(
|
||||
<div style={{font: 'qux', fontStyle: 'bar'}} />,
|
||||
container,
|
||||
);
|
||||
ReactDOM.render(
|
||||
<div style={{font: 'foo', fontStyle: 'baz'}} />,
|
||||
container,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
ReactDOM.render(
|
||||
<div style={{font: 'qux', fontStyle: 'baz'}} />,
|
||||
container,
|
||||
),
|
||||
).toWarnDev(
|
||||
'Warning: Updating a style property during rerender (font) when ' +
|
||||
'a conflicting property is set (fontStyle) can lead to styling ' +
|
||||
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
|
||||
'properties for the same value; instead, replace the shorthand ' +
|
||||
'with separate values.' +
|
||||
'\n in div (at **)',
|
||||
);
|
||||
expect(() =>
|
||||
ReactDOM.render(<div style={{fontStyle: 'baz'}} />, container),
|
||||
).toWarnDev(
|
||||
'Warning: Removing a style property during rerender (font) when ' +
|
||||
'a conflicting property is set (fontStyle) can lead to styling ' +
|
||||
"bugs. To avoid this, don't mix shorthand and non-shorthand " +
|
||||
'properties for the same value; instead, replace the shorthand ' +
|
||||
'with separate values.' +
|
||||
'\n in div (at **)',
|
||||
);
|
||||
|
||||
// A bit of a special case: backgroundPosition isn't technically longhand
|
||||
// (it expands to backgroundPosition{X,Y} but so does background)
|
||||
ReactDOM.render(
|
||||
<div style={{background: 'yellow', backgroundPosition: 'center'}} />,
|
||||
container,
|
||||
);
|
||||
expect(() =>
|
||||
ReactDOM.render(<div style={{background: 'yellow'}} />, container),
|
||||
).toWarnDev(
|
||||
'Warning: Removing a style property during rerender ' +
|
||||
'(backgroundPosition) when a conflicting property is set ' +
|
||||
"(background) can lead to styling bugs. To avoid this, don't mix " +
|
||||
'shorthand and non-shorthand properties for the same value; ' +
|
||||
'instead, replace the shorthand with separate values.' +
|
||||
'\n in div (at **)',
|
||||
);
|
||||
ReactDOM.render(
|
||||
<div style={{background: 'yellow', backgroundPosition: 'center'}} />,
|
||||
container,
|
||||
);
|
||||
// But setting them at the same time is OK:
|
||||
ReactDOM.render(
|
||||
<div style={{background: 'green', backgroundPosition: 'top'}} />,
|
||||
container,
|
||||
);
|
||||
expect(() =>
|
||||
ReactDOM.render(<div style={{backgroundPosition: 'top'}} />, container),
|
||||
).toWarnDev(
|
||||
'Warning: Removing a style property during rerender (background) ' +
|
||||
'when a conflicting property is set (backgroundPosition) can lead ' +
|
||||
"to styling bugs. To avoid this, don't mix shorthand and " +
|
||||
'non-shorthand properties for the same value; instead, replace the ' +
|
||||
'shorthand with separate values.' +
|
||||
'\n in div (at **)',
|
||||
);
|
||||
|
||||
// A bit of an even more special case: borderLeft and borderStyle overlap.
|
||||
ReactDOM.render(
|
||||
<div style={{borderStyle: 'dotted', borderLeft: '1px solid red'}} />,
|
||||
container,
|
||||
);
|
||||
expect(() =>
|
||||
ReactDOM.render(
|
||||
<div style={{borderLeft: '1px solid red'}} />,
|
||||
container,
|
||||
),
|
||||
).toWarnDev(
|
||||
'Warning: Removing a style property during rerender (borderStyle) ' +
|
||||
'when a conflicting property is set (borderLeft) can lead to ' +
|
||||
"styling bugs. To avoid this, don't mix shorthand and " +
|
||||
'non-shorthand properties for the same value; instead, replace the ' +
|
||||
'shorthand with separate values.' +
|
||||
'\n in div (at **)',
|
||||
);
|
||||
expect(() =>
|
||||
ReactDOM.render(
|
||||
<div style={{borderStyle: 'dashed', borderLeft: '1px solid red'}} />,
|
||||
container,
|
||||
),
|
||||
).toWarnDev(
|
||||
'Warning: Updating a style property during rerender (borderStyle) ' +
|
||||
'when a conflicting property is set (borderLeft) can lead to ' +
|
||||
"styling bugs. To avoid this, don't mix shorthand and " +
|
||||
'non-shorthand properties for the same value; instead, replace the ' +
|
||||
'shorthand with separate values.' +
|
||||
'\n in div (at **)',
|
||||
);
|
||||
// But setting them at the same time is OK:
|
||||
ReactDOM.render(
|
||||
<div style={{borderStyle: 'dotted', borderLeft: '2px solid red'}} />,
|
||||
container,
|
||||
);
|
||||
expect(() =>
|
||||
ReactDOM.render(<div style={{borderStyle: 'dotted'}} />, container),
|
||||
).toWarnDev(
|
||||
'Warning: Removing a style property during rerender (borderLeft) ' +
|
||||
'when a conflicting property is set (borderStyle) can lead to ' +
|
||||
"styling bugs. To avoid this, don't mix shorthand and " +
|
||||
'non-shorthand properties for the same value; instead, replace the ' +
|
||||
'shorthand with separate values.' +
|
||||
'\n in div (at **)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn for unknown prop', () => {
|
||||
const container = document.createElement('div');
|
||||
expect(() =>
|
||||
|
||||
@@ -766,6 +766,12 @@ export function diffProperties(
|
||||
}
|
||||
}
|
||||
if (styleUpdates) {
|
||||
if (__DEV__) {
|
||||
CSSPropertyOperations.validateShorthandPropertyCollisionInDev(
|
||||
styleUpdates,
|
||||
nextProps[STYLE],
|
||||
);
|
||||
}
|
||||
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
|
||||
}
|
||||
return updatePayload;
|
||||
|
||||
@@ -5,9 +5,16 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
overlappingShorthandsInDev,
|
||||
longhandToShorthandInDev,
|
||||
shorthandToLonghandInDev,
|
||||
} from './CSSShorthandProperty';
|
||||
|
||||
import dangerousStyleValue from './dangerousStyleValue';
|
||||
import hyphenateStyleName from './hyphenateStyleName';
|
||||
import warnValidStyle from './warnValidStyle';
|
||||
import warning from 'shared/warning';
|
||||
|
||||
/**
|
||||
* Operations for dealing with CSS properties.
|
||||
@@ -78,3 +85,92 @@ export function setValueForStyles(node, styles) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isValueEmpty(value) {
|
||||
return value == null || typeof value === 'boolean' || value === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* When mixing shorthand and longhand property names, we warn during updates if
|
||||
* we expect an incorrect result to occur. In particular, we warn for:
|
||||
*
|
||||
* Updating a shorthand property (longhand gets overwritten):
|
||||
* {font: 'foo', fontVariant: 'bar'} -> {font: 'baz', fontVariant: 'bar'}
|
||||
* becomes .style.font = 'baz'
|
||||
* Removing a shorthand property (longhand gets lost too):
|
||||
* {font: 'foo', fontVariant: 'bar'} -> {fontVariant: 'bar'}
|
||||
* becomes .style.font = ''
|
||||
* Removing a longhand property (should revert to shorthand; doesn't):
|
||||
* {font: 'foo', fontVariant: 'bar'} -> {font: 'foo'}
|
||||
* becomes .style.fontVariant = ''
|
||||
*/
|
||||
export function validateShorthandPropertyCollisionInDev(
|
||||
styleUpdates,
|
||||
nextStyles,
|
||||
) {
|
||||
if (!nextStyles) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in styleUpdates) {
|
||||
const isEmpty = isValueEmpty(styleUpdates[key]);
|
||||
if (isEmpty) {
|
||||
// Property removal; check if we're removing a longhand property
|
||||
const shorthands = longhandToShorthandInDev[key];
|
||||
if (shorthands) {
|
||||
const conflicting = shorthands.filter(
|
||||
s => !isValueEmpty(nextStyles[s]),
|
||||
);
|
||||
if (conflicting.length) {
|
||||
warning(
|
||||
false,
|
||||
'Removing a style property during rerender (%s) when a ' +
|
||||
'conflicting property is set (%s) can lead to styling bugs. To ' +
|
||||
"avoid this, don't mix shorthand and non-shorthand properties " +
|
||||
'for the same value; instead, replace the shorthand with ' +
|
||||
'separate values.',
|
||||
key,
|
||||
conflicting.join(', '),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Updating or removing a property; check if it's a shorthand property
|
||||
const longhands = shorthandToLonghandInDev[key];
|
||||
const overlapping = overlappingShorthandsInDev[key];
|
||||
// eslint-disable-next-line no-var
|
||||
var conflicting = new Set();
|
||||
if (longhands) {
|
||||
longhands.forEach(l => {
|
||||
if (isValueEmpty(styleUpdates[l]) && !isValueEmpty(nextStyles[l])) {
|
||||
// ex: key = 'font', l = 'fontStyle'
|
||||
conflicting.add(l);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (overlapping) {
|
||||
overlapping.forEach(l => {
|
||||
if (isValueEmpty(styleUpdates[l]) && !isValueEmpty(nextStyles[l])) {
|
||||
// ex: key = 'borderLeft', l = 'borderStyle'
|
||||
conflicting.add(l);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (conflicting.size) {
|
||||
warning(
|
||||
false,
|
||||
'%s a style property during rerender (%s) when a ' +
|
||||
'conflicting property is set (%s) can lead to styling bugs. To ' +
|
||||
"avoid this, don't mix shorthand and non-shorthand properties " +
|
||||
'for the same value; instead, replace the shorthand with ' +
|
||||
'separate values.',
|
||||
isEmpty ? 'Removing' : 'Updating',
|
||||
key,
|
||||
Array.from(conflicting)
|
||||
.sort()
|
||||
.join(', '),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// List derived from Gecko source code:
|
||||
// https://github.com/mozilla/gecko-dev/blob/4e638efc71/layout/style/test/property_database.js
|
||||
export const shorthandToLonghandInDev = __DEV__
|
||||
? {
|
||||
animation: [
|
||||
'animationDelay',
|
||||
'animationDirection',
|
||||
'animationDuration',
|
||||
'animationFillMode',
|
||||
'animationIterationCount',
|
||||
'animationName',
|
||||
'animationPlayState',
|
||||
'animationTimingFunction',
|
||||
],
|
||||
background: [
|
||||
'backgroundAttachment',
|
||||
'backgroundClip',
|
||||
'backgroundColor',
|
||||
'backgroundImage',
|
||||
'backgroundOrigin',
|
||||
'backgroundPositionX',
|
||||
'backgroundPositionY',
|
||||
'backgroundRepeat',
|
||||
'backgroundSize',
|
||||
],
|
||||
backgroundPosition: ['backgroundPositionX', 'backgroundPositionY'],
|
||||
border: [
|
||||
'borderBottomColor',
|
||||
'borderBottomStyle',
|
||||
'borderBottomWidth',
|
||||
'borderImageOutset',
|
||||
'borderImageRepeat',
|
||||
'borderImageSlice',
|
||||
'borderImageSource',
|
||||
'borderImageWidth',
|
||||
'borderLeftColor',
|
||||
'borderLeftStyle',
|
||||
'borderLeftWidth',
|
||||
'borderRightColor',
|
||||
'borderRightStyle',
|
||||
'borderRightWidth',
|
||||
'borderTopColor',
|
||||
'borderTopStyle',
|
||||
'borderTopWidth',
|
||||
],
|
||||
borderBlockEnd: [
|
||||
'borderBlockEndColor',
|
||||
'borderBlockEndStyle',
|
||||
'borderBlockEndWidth',
|
||||
],
|
||||
borderBlockStart: [
|
||||
'borderBlockStartColor',
|
||||
'borderBlockStartStyle',
|
||||
'borderBlockStartWidth',
|
||||
],
|
||||
borderBottom: [
|
||||
'borderBottomColor',
|
||||
'borderBottomStyle',
|
||||
'borderBottomWidth',
|
||||
],
|
||||
borderColor: [
|
||||
'borderBottomColor',
|
||||
'borderLeftColor',
|
||||
'borderRightColor',
|
||||
'borderTopColor',
|
||||
],
|
||||
borderImage: [
|
||||
'borderImageOutset',
|
||||
'borderImageRepeat',
|
||||
'borderImageSlice',
|
||||
'borderImageSource',
|
||||
'borderImageWidth',
|
||||
],
|
||||
borderInlineEnd: [
|
||||
'borderInlineEndColor',
|
||||
'borderInlineEndStyle',
|
||||
'borderInlineEndWidth',
|
||||
],
|
||||
borderInlineStart: [
|
||||
'borderInlineStartColor',
|
||||
'borderInlineStartStyle',
|
||||
'borderInlineStartWidth',
|
||||
],
|
||||
borderLeft: ['borderLeftColor', 'borderLeftStyle', 'borderLeftWidth'],
|
||||
borderRadius: [
|
||||
'borderBottomLeftRadius',
|
||||
'borderBottomRightRadius',
|
||||
'borderTopLeftRadius',
|
||||
'borderTopRightRadius',
|
||||
],
|
||||
borderRight: ['borderRightColor', 'borderRightStyle', 'borderRightWidth'],
|
||||
borderStyle: [
|
||||
'borderBottomStyle',
|
||||
'borderLeftStyle',
|
||||
'borderRightStyle',
|
||||
'borderTopStyle',
|
||||
],
|
||||
borderTop: ['borderTopColor', 'borderTopStyle', 'borderTopWidth'],
|
||||
borderWidth: [
|
||||
'borderBottomWidth',
|
||||
'borderLeftWidth',
|
||||
'borderRightWidth',
|
||||
'borderTopWidth',
|
||||
],
|
||||
columnRule: ['columnRuleColor', 'columnRuleStyle', 'columnRuleWidth'],
|
||||
columns: ['columnCount', 'columnWidth'],
|
||||
flex: ['flexBasis', 'flexGrow', 'flexShrink'],
|
||||
flexFlow: ['flexDirection', 'flexWrap'],
|
||||
font: [
|
||||
'fontFamily',
|
||||
'fontFeatureSettings',
|
||||
'fontKerning',
|
||||
'fontLanguageOverride',
|
||||
'fontSize',
|
||||
'fontSizeAdjust',
|
||||
'fontStretch',
|
||||
'fontStyle',
|
||||
'fontVariant',
|
||||
'fontVariantAlternates',
|
||||
'fontVariantCaps',
|
||||
'fontVariantEastAsian',
|
||||
'fontVariantLigatures',
|
||||
'fontVariantNumeric',
|
||||
'fontVariantPosition',
|
||||
'fontWeight',
|
||||
'lineHeight',
|
||||
],
|
||||
fontVariant: [
|
||||
'fontVariantAlternates',
|
||||
'fontVariantCaps',
|
||||
'fontVariantEastAsian',
|
||||
'fontVariantLigatures',
|
||||
'fontVariantNumeric',
|
||||
'fontVariantPosition',
|
||||
],
|
||||
gap: ['columnGap', 'rowGap'],
|
||||
grid: [
|
||||
'gridAutoColumns',
|
||||
'gridAutoFlow',
|
||||
'gridAutoRows',
|
||||
'gridTemplateAreas',
|
||||
'gridTemplateColumns',
|
||||
'gridTemplateRows',
|
||||
],
|
||||
gridArea: [
|
||||
'gridColumnEnd',
|
||||
'gridColumnStart',
|
||||
'gridRowEnd',
|
||||
'gridRowStart',
|
||||
],
|
||||
gridColumn: ['gridColumnEnd', 'gridColumnStart'],
|
||||
gridColumnGap: ['columnGap'],
|
||||
gridGap: ['columnGap', 'rowGap'],
|
||||
gridRow: ['gridRowEnd', 'gridRowStart'],
|
||||
gridRowGap: ['rowGap'],
|
||||
gridTemplate: [
|
||||
'gridTemplateAreas',
|
||||
'gridTemplateColumns',
|
||||
'gridTemplateRows',
|
||||
],
|
||||
listStyle: ['listStyleImage', 'listStylePosition', 'listStyleType'],
|
||||
margin: ['marginBottom', 'marginLeft', 'marginRight', 'marginTop'],
|
||||
marker: ['markerEnd', 'markerMid', 'markerStart'],
|
||||
mask: [
|
||||
'maskClip',
|
||||
'maskComposite',
|
||||
'maskImage',
|
||||
'maskMode',
|
||||
'maskOrigin',
|
||||
'maskPositionX',
|
||||
'maskPositionY',
|
||||
'maskRepeat',
|
||||
'maskSize',
|
||||
],
|
||||
maskPosition: ['maskPositionX', 'maskPositionY'],
|
||||
outline: ['outlineColor', 'outlineStyle', 'outlineWidth'],
|
||||
overflow: ['overflowX', 'overflowY'],
|
||||
padding: ['paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop'],
|
||||
placeContent: ['alignContent', 'justifyContent'],
|
||||
placeItems: ['alignItems', 'justifyItems'],
|
||||
placeSelf: ['alignSelf', 'justifySelf'],
|
||||
textDecoration: [
|
||||
'textDecorationColor',
|
||||
'textDecorationLine',
|
||||
'textDecorationStyle',
|
||||
],
|
||||
textEmphasis: ['textEmphasisColor', 'textEmphasisStyle'],
|
||||
transition: [
|
||||
'transitionDelay',
|
||||
'transitionDuration',
|
||||
'transitionProperty',
|
||||
'transitionTimingFunction',
|
||||
],
|
||||
wordWrap: ['overflowWrap'],
|
||||
}
|
||||
: {};
|
||||
|
||||
export const longhandToShorthandInDev = {};
|
||||
|
||||
export const overlappingShorthandsInDev = {};
|
||||
|
||||
/**
|
||||
* obj[key].push(val), handling new keys.
|
||||
*/
|
||||
function pushToValueArray(obj, key, val) {
|
||||
let arr = obj[key];
|
||||
if (!arr) {
|
||||
obj[key] = arr = [];
|
||||
}
|
||||
arr.push(val);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
// eslint-disable-next-line no-var
|
||||
var shorthand;
|
||||
|
||||
// We want to treat properties like 'backgroundPosition' as one of the
|
||||
// properties that 'background' expands to, even though that's technically
|
||||
// incorrect.
|
||||
for (shorthand in shorthandToLonghandInDev) {
|
||||
const longhands = shorthandToLonghandInDev[shorthand];
|
||||
for (const shorthand2 in shorthandToLonghandInDev) {
|
||||
// eslint-disable-next-line no-var
|
||||
var longhands2 = shorthandToLonghandInDev[shorthand2];
|
||||
if (shorthand <= shorthand2) {
|
||||
continue;
|
||||
}
|
||||
if (longhands.every(l => l === shorthand || longhands2.includes(l))) {
|
||||
// ex: shorthand = 'backgroundPosition', shorthand2 = 'background'
|
||||
longhands2.push(shorthand);
|
||||
} else if (
|
||||
longhands.some(l => l !== shorthand && longhands2.includes(l))
|
||||
) {
|
||||
// ex: shorthand = 'borderLeft', shorthand2 = 'borderStyle'
|
||||
pushToValueArray(overlappingShorthandsInDev, shorthand, shorthand2);
|
||||
pushToValueArray(overlappingShorthandsInDev, shorthand2, shorthand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flip into {maskPositionX: ['mask', 'maskPosition']} for reverse lookups
|
||||
for (shorthand in shorthandToLonghandInDev) {
|
||||
const longhands = shorthandToLonghandInDev[shorthand];
|
||||
longhands.forEach(longhand => {
|
||||
pushToValueArray(longhandToShorthandInDev, longhand, shorthand);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user