react icon indicating copy to clipboard operation
react copied to clipboard

Bug: CSS variables can't be a space character

Open itsdouges opened this issue 4 years ago • 12 comments

React version: v16/v17

Steps To Reproduce

  1. https://codesandbox.io/s/empty-css-value-jmbfl?file=/src/App.js
  2. Notice the "should be black border" has a pink border
  3. Dangerously setting inner HTML works because it doesn't go through reacts trim() flow
  4. I've tracked the suspect code to here https://github.com/facebook/react/blob/6cbb9394d1474e3a728b49dc7f3a11d61a421ce3/packages/react-dom/src/shared/dangerousStyleValue.js#L44

Link to code example: https://codesandbox.io/s/empty-css-value-jmbfl?file=/src/App.js

The current behavior

CSS variables have their value trimmed - resulting in ' ' become '' which then removes the CSS variable from the browser.

image

The expected behavior

The ' ' value should not be trimmed.

image

One potential option is to, for any custom properties that have a space value, don't trim.

if (isCustomProperty && value === ' ') {
   return value;
}

return ('' + value).trim();

I'm happy to do the bug fix. I would also love for this to get released on v16.


Found a work around:

https://codesandbox.io/s/empty-css-value-forked-g5jud?file=/src/App.js

style={{ "--border-color": "var(--, )" }}

itsdouges avatar Dec 22 '20 04:12 itsdouges

My thoughts are that style values which, when trimmed, become empty strings, should be returned unaltered, i.e.:

if (value.toString().trim() === ‘’) return value;

This would handle the space, series of spaces, and also the case where an empty string is supplied.

ctjlewis avatar Dec 22 '20 05:12 ctjlewis

We've found a work around: https://codesandbox.io/s/empty-css-value-forked-g5jud?file=/src/App.js

style={{ "--border-color": "var(--, )" }}

Not the most ideal - but it works 😄.

itsdouges avatar Dec 22 '20 09:12 itsdouges

Interestingly, trimming was intentionally added and considered "correct" (see commit message): https://github.com/facebook/react/commit/2bff5c5c7e776a183ed9b429f2cb46cc044d9bc1.

Considering that whitespace is a valid token for custom properties (2. Defining Custom Properties: the --* family of properties -> 8.2. Defining Arbitrary Contents: the and productions), it doesn't seem like trimming is correct.

Maybe it was added to fix a browser quirk? Would be interesting to see if the described quirk still happens in React 17 without trimming the values.

eps1lon avatar Dec 22 '20 10:12 eps1lon

I can't replicate the suffix being added (both v16 + v17 behave the same - see: https://codesandbox.io/s/solitary-pond-v0i69?file=/src/App.js) - I wonder what use case it's wanting to handle?

itsdouges avatar Dec 22 '20 10:12 itsdouges

I can't replicate the suffix being added (both v16 + v17 behave the same - see: codesandbox.io/s/solitary-pond-v0i69?file=/src/App.js) - I wonder what use case it's wanting to handle?

Maybe this changed over time. Unitless values as strings aren't working at all on the client. For example:

https://github.com/facebook/react/blob/6cbb9394d1474e3a728b49dc7f3a11d61a421ce3/packages/react-dom/src/tests/CSSPropertyOperations-test.js#L29-L38

right:4 is created on the server but has no effect on the client. It doesn't matter whether the value is trimmed or not.

Have you tried removing the trimming behavior and see what test fails?

Edit: related spec sections: left property definition allows <length> as a value. However, <length> requires a unit unless it is 0. So https://github.com/facebook/react/blob/6cbb9394d1474e3a728b49dc7f3a11d61a421ce3/packages/react-dom/src/tests/CSSPropertyOperations-test.js#L29-L38 is not testing behavior that is relevant to browsers (though there's always the possibility react supports some browser quirk).

eps1lon avatar Dec 22 '20 15:12 eps1lon

Ok checking the code out and removing the trim I get only that test you've mentioned failing:

Summary of all failing tests
 FAIL  packages/react-dom/src/__tests__/CSSPropertyOperations-test.js
  ● CSSPropertyOperations › should trim values

    expect(received).toContain(expected) // indexOf

    Expected substring: "\"left:16;opacity:0.5;right:4\""
    Received string:    "<div style=\"left:16 ;opacity:0.5;right: 4 \" data-reactroot=\"\"></div>"

      35 |     const div = <div style={styles} />;
      36 |     const html = ReactDOMServer.renderToString(div);
    > 37 |     expect(html).toContain('"left:16;opacity:0.5;right:4"');
         |                  ^
      38 |   });
      39 |
      40 |   it('should not append `px` to styles that might need a number', () => {

      at Object.<anonymous> (packages/react-dom/src/__tests__/CSSPropertyOperations-test.js:37:18)


Test Suites: 1 failed, 257 passed, 258 total
Tests:       1 failed, 26 skipped, 7009 passed, 7036 total
Snapshots:   83 passed, 83 total
Time:        165.044s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Command failed: /usr/local/Cellar/yvm/3.3.0/versions/v1.22.4/bin/yarn.js test
➜  react git:(master) ✗

I have to wonder if we can just skip the trim if the value is a space. Or whitespace.

itsdouges avatar Dec 22 '20 22:12 itsdouges

As far as I know this test isn't testing anything meaningful for browsers since right:4 and left:16 are invalid i.e. won't have any effect.

I would replace the unitless numbers with a zero (e.g. left: ' 0 ') and test the generated markup in a browser

eps1lon avatar Dec 23 '20 10:12 eps1lon

0 still gets applied with extra white space (heres just a quick vanilla sandbox)

image image

https://codesandbox.io/s/naughty-star-y43ey?file=/index.html

do we have enough to go on? what do we want for more evidence or investigation 😄

itsdouges avatar Dec 31 '20 22:12 itsdouges

what do we want for more evidence or investigation 😄

I guess propose the change via PR and then we'll see what core members think about this change.

eps1lon avatar Jan 01 '21 12:01 eps1lon

style={{ "--border-color": "black" }}, also solves the problem

subhamdas461 avatar Jan 09 '21 18:01 subhamdas461

In this instance yes, however the behaviour wanted is to actually "unset" the CSS variable, having the border change to black is merely the effect of unsetting.

itsdouges avatar Jan 09 '21 21:01 itsdouges

Ran into a permutation of this issue this week, in our case we were setting the value of a custom property to an empty string:

<div
  style={{
    '--foo': ''
  }}
/>

When server side rendering, it would be stripped so the returned html to the client would look like:

<div></div>

and then cause a client server mismatch because on the client it was being respected as a value, expecting the following html:

<div style="--foo:;"></div>

We were able to workaround this issue by setting the value of the variable to '0' in our case, there is also some client side code (dependent upon useState/useEffect/etc) that updated the value of the custom property later during rendering, so the default of 0 worked for us.

Looking more at the code, which I think moved to here: https://github.com/facebook/react/blob/2cf4352e1c81a5b8c3528519a128c20e8e65531d/packages/react-dom-bindings/src/shared/dangerousStyleValue.js, I wonder if in all cases of the key being a custom property, that the code should let the value pass through as-is, without trimming it/changing it.

In our case, we were running into this branch specifically: https://github.com/facebook/react/blob/2cf4352e1c81a5b8c3528519a128c20e8e65531d/packages/react-dom-bindings/src/shared/dangerousStyleValue.js#L31-L34

So I think the ideal change would look something like:

function dangerousStyleValue(name, value, isCustomProperty) {
  if (isCustomProperty) {
    // I assume we'd want to preserve this check as well, but haven't dug into the code much to see if its necessary
    if (__DEV__) {
      checkCSSPropertyStringCoercion(value, name);
    }
    return value;
  }
  // ... current code here

@madou have you prepared a PR for any of the proposed changes at all? Curious if my snippet above would also work for your case too!

hamlim avatar Oct 12 '22 13:10 hamlim