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

[system] Use cx instead of clsx #32067

Closed
wants to merge 3 commits into from
Closed

[system] Use cx instead of clsx #32067

wants to merge 3 commits into from

Conversation

garronej
Copy link
Contributor

@garronej garronej commented Mar 31, 2022

Hi @mnajdova,

Following our discussion there is a PR to show you what I have in mind.

You can run the test with:

yarn test:regressions:dev

Then access: http://localhost:3000/regression-Cx/Cx#no-dev

With this branch you'll get:
image

With mui/material-ui#master:
image

Summary, for the record, of why I am doing this:

  1. To enable @emotion/react to be used for writing custom-style. See PR#31825.
  2. To make @emotion/css work as expected when user to write custom styles.
  3. Enable TSS to work out of the box with MUI, without user having to explicitly provide a cache and follow specific instructions for SSR.

I am looking forward to reading what you think of it.

Best regard,

@mui-bot
Copy link

mui-bot commented Mar 31, 2022

Details of bundle changes

@material-ui/core: parsed: +0.15% , gzip: +0.24%
@material-ui/lab: parsed: +0.20% , gzip: +0.26%
@mui/material-next: parsed: +0.61% , gzip: +0.82%

Generated by 🚫 dangerJS against bd80530

@danilo-leal danilo-leal changed the title Use cx instead of clsx [system] Use cx instead of clsx Mar 31, 2022
@danilo-leal danilo-leal added the package: system Specific to @mui/system label Mar 31, 2022
@garronej
Copy link
Contributor Author

garronej commented Apr 1, 2022

I realize that this PR might be a bit hard to grasp. Let me give you some additional technical details.

  • @emotion/css export cx. It's like clsx but it's able to ensures styles are overwritten in the correct order (among styles generated by emotion with a given cache).
  • The cx function is exported only from @emotion/css but not from @emotion/react. Yet mui-styled-engine depends on @emotioin/react, we don't want to add an extra dependency so we recreate cx with what's available in @emotion/react.
  • Il order for cx to work we need the contextual emotion cache or whatever cache is used by default. I got an API in @emotion/react that enables us to pick it up: __unsafe_useEmotionCache.
  • In the bowser __unsafe_useEmotionCache() never returns null. On the server, if the user wants SSR to be working, he has to explicitly provide an emotion cache. There is no way around it. Thus we can be sure that we always have the relevant cache.
  • This is just the implementation of the well known classenames function. We could very well use clsx instead. I copy pasted the implementation from @emotion/react because it doesn't export it and I wanted to avoid adding an extra dependency to mui-styled-engine and mui-styled-engine-sc.
  • mui-styled-engine-sc needs to have the same API than mui-styled-engine, yet styled-component isn't able, like emotion is, to ensure styles are overwritten in the correct order. When mui-styled-engine-sc is used useCx only returns an implementation of the classnames API with no extra smart.
  • Of course the change mades to Button (use of cx in place of clsx) would have to be applied to every component. I didn't do it yet because this PR should be enough to start the discussion.
  • There is a block of code that modify the default behaviour of @emotion/css's cx. It is related to this issue users faced upgrading from v4. To make it simple, if we use the default cx behaviour, in this image the background is red because of the higher specificity induced by the media query. I think the expected behaviour is for the background to be green.

Best regards,

Copy link
Member

@mnajdova mnajdova left a comment

Choose a reason for hiding this comment

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

The changes more or less make sense (I would kill the fix for the media queries for now, but I already left a comment for it below, so we can continue the thread there).

Regarding the test-case, note that also plan to add a new API (coming from the @mui/base package), where we would have componentsProps.slot and we would need to merge the className prop from there in the correct order too (for the @mui/base components we don't plan to suppor the classes prop, only the componentsProps API, but for the @mui/material components I wouldn't kill the classes API yet, to avoid numeros breaking changes again). Anyway, the fix would apply if the css prop is used there too, so this fact shouldn't change much.

One scary thought is that we would need to update all components, which always comes with the risk of breaking something along the way. I am curious to hear what @siriwatknp's thoughts on the changes too.

packages/mui-styled-engine-sc/src/cx.js Outdated Show resolved Hide resolved
packages/mui-styled-engine-sc/src/tools/classnames.js Outdated Show resolved Hide resolved
}

// https://github.com/garronej/tss-react/issues/27
const increaseSpecificityToTakePrecedenceOverMediaQueries = (() => {
Copy link
Member

Choose a reason for hiding this comment

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

This looks suspicious. I understand that it fixes an issue that was reported, but would it be better if this is reported inside emotion and we get a validation from the authors there that this is a bug? Increasing the specificity behind the scenes sounds scary.

Copy link
Contributor Author

@garronej garronej Apr 11, 2022

Choose a reason for hiding this comment

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

There is no 'bug' strictly speaking in the default cx, it works as the specs says it work; but not as the users expect it to work.
Whether or not the increase of specificity to override media queries should be the default behavior of cx is up to debate.
For TSS I had no choice but to implement the feature. Users were able to override media queries in v4 so I had to implement it or it would have made it very hard for some projects to upgrade to MUIv5.

I personally think that having this increase of specificity is a hard requirement for us.
If I have a button that, by default, is red for large screens and blue otherwise and I decide to make it white by passing a custom class. I expect my button to be white regardless of the screen size.

If I kill this feature MUI and TSS will end up with an avalanche of issues like "BUG: My custom style only applies on small screen size".

Copy link
Member

Choose a reason for hiding this comment

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

Interesting, I've tried replicating this with pure CSS, and looks like it works as you are suggesting (if we assume the order of the classes in the CSS file I have would correspond with the order of the classes added in the cx util): https://codepen.io/mnajdova/pen/VwyBEyX

@Andarist what do you think about this? I remember we once discussed this and you had a push back on it with some reason, but I don't remember what it was. I couldn't find the issue where it was discussed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting, I've tried replicating this with pure CSS, and looks like it works as you are suggesting (if we assume the order of the classes in the CSS file I have would correspond with the order of the classes added in the cx util): https://codepen.io/mnajdova/pen/VwyBEyX

Yeah, this is how it already works in Emotion because we just "flatten" everything into a single rule~. Media query doesn't increase the specificity anyhow so it "just works". However, at times this might be confusing because sometimes people only want to override the "base" declaration without affecting the media query one and there is no easy way to do this.

This might depend on how exactly you merge styles here, if you nest them etc. I would appreciate a codesandbox or something of the "broken" behavior in MUI - then I could perhaps comment further because right now I don't fully understand it as the basic case should work exactly like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @Andarist, thank you for your involvement,
I'll fix you a sandbox ASAP

Copy link
Contributor Author

@garronej garronej May 9, 2022

Choose a reason for hiding this comment

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

I mean, I'd really love to be proven wrong but I dont see any other way to make it work but to use cx.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've successfully rendered this text as pink with just those two patches applied to your repro (this one):

Patch 1
diff --git a/node_modules/@mui/base/composeClasses/composeClasses.js b/node_modules/@mui/base/composeClasses/composeClasses.js
index 8d25ea4..2382b9c 100644
--- a/node_modules/@mui/base/composeClasses/composeClasses.js
+++ b/node_modules/@mui/base/composeClasses/composeClasses.js
@@ -3,17 +3,24 @@ export default function composeClasses(slots, getUtilityClass, classes) {
   Object.keys(slots).forEach( // `Objet.keys(slots)` can't be wider than `T` because we infer `T` from `slots`.
   // @ts-expect-error https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208
   slot => {
-    output[slot] = slots[slot].reduce((acc, key) => {
+    const arr = slots[slot].reduce((acc, key) => {
+      if (key) {
+        acc.push(getUtilityClass(key));
+      }
+
+      return acc;
+    }, [])
+
+    slots[slot].forEach((key) => {
       if (key) {
         if (classes && classes[key]) {
-          acc.push(classes[key]);
+          arr.push(classes[key]);
         }
 
-        acc.push(getUtilityClass(key));
       }
+    })
 
-      return acc;
-    }, []).join(' ');
+    output[slot] = arr.join(' ');
   });
   return output;
 }
Patch 2
diff --git a/node_modules/@mui/material/ButtonBase/ButtonBase.js b/node_modules/@mui/material/ButtonBase/ButtonBase.js
index f2333f8..407818e 100644
--- a/node_modules/@mui/material/ButtonBase/ButtonBase.js
+++ b/node_modules/@mui/material/ButtonBase/ButtonBase.js
@@ -321,7 +321,7 @@ const ButtonBase = /*#__PURE__*/React.forwardRef(function ButtonBase(inProps, re
   const classes = useUtilityClasses(ownerState);
   return /*#__PURE__*/_jsxs(ButtonBaseRoot, _extends({
     as: ComponentProp,
-    className: clsx(classes.root, className),
+    className: clsx(className, classes.root),
     ownerState: ownerState,
     onBlur: handleBlur,
     onClick: onClick,

Take a look at the generated class name, now the color: pink; is at the very bottom:
Screenshot 2022-05-09 at 19 30 16

I don't advise actually using those patches, this is just a quick hack - the point is that this can be fixed without using cx. I lack the knowledge about the internal composition of styles and "slots" to know how to actually this should be fixed.

And actually... the first patch isn't even needed here but I feel more comfortable with the logic included in it. OTOH, I don't entirely understand why the composeClasses is inserting that root class name there if it ends up being provided directly~ to the className prop in the ButtonBase anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for taking the time to prove your point.
I was stuck on the idea that:

clsx(classes.root, className) === `${classes.root} ${className}`

which I assumed to be equivalent to

`${classname} ${classes.root}`

I thought only cx was able to enforce priority of one class over another.

Turns out I was wrong, I am not familiar enough with the inner working of emotion.

It’s good news.

Copy link
Contributor

Choose a reason for hiding this comment

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

What u are saying is correct but in this case the resulting className is passed to Emotion and it can still handle decoding that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok thx, make sense!

@siriwatknp siriwatknp self-assigned this Apr 12, 2022
@siriwatknp
Copy link
Member

FYI, this is on my radar. I might take a few days to get more context, will share my thought after that.

@garronej
Copy link
Contributor Author

garronej commented May 8, 2022

Hi @mnajdova @siriwatknp,
I just applied the suggestion of @mnajdova.

A lot have been said but the important pointers to understand this PR are in this message.

I still think it's a problem for us that emotion, unlike vanilla CSS, assigns a higher specificity to rules within media queries but regardless, this effort is still undoubtedly worthy to be pursued.
Besides, maybe, while being a problem in theory, it won't be a problem in practice.

Best regards,

UPDATE:
Latest update from @Andarist suggests that there might be a simpler way to achieve the same result.
I'll investigate.

@mnajdova
Copy link
Member

UPDATE:
Latest update from @Andarist suggests that there might be a simpler way to achieve the same result.
I'll investigate.

@garronej let me know if you have any updates on this.

@garronej
Copy link
Contributor Author

let me know if you have any updates on this.

@mnajdova Sure, I didn't give up in this, I just need to make time...

@garronej
Copy link
Contributor Author

Hi @mnajdova,
There is no need to use cx indeed.
We just need to make sure Emotion receive the classeName in the correct order and that's it!

I've opened a new pr

@garronej garronej closed this Jun 18, 2022
@mnajdova
Copy link
Member

This is great news! I am happy we can have a simple fix :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
package: system Specific to @mui/system
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants