I recently started a React + Material-UI project. I naturally chose CSS modules (SASS modules to be exact) for styling over CSS-in-JS because I was more familiar with CSS/SASS. I also added typings-for-css-modules-loader
in Webpack config for adding typing in TypeScript.
Then I started to realize a few issues.
For example, Webpack will generate .d.ts
file automatically, but not that reliable. Sometimes I need to go back and forth and save (which triggers compiling and hot reloading) the component file itself, the component file which imports this component, and the CSS file.
But one other issue raises the red flag for me: customizing/overriding MUI styling is painful using CSS Modules.
For example, I have a simple icon button, but I need it to be smaller than the MUI default. And this is the code:
// MyIcon.tsx
import * as React from 'react';
import IconButton from '@material-ui/core/IconButton';
import Close from '@material-ui/icons/Close';
import * as styles from './MyIcon.module.scss';const MyIcon: React.FunctionComponent = () => (
<IconButton className={styles.button}>
<Close />
</IconButton>
);export default MyIcon;// MyIcon.module.scss
.button {
padding: 0 0.5em !important;
color: purple !important;
min-width: 0;
}// MyIcon.module.scss.d.ts
export const button: string;
See those !important
? My styles won’t work without it. Why? Because MUI styling somehow has higher precedence. And sometimes it begins a guessing game, should I add or not?
I was told there might be a solution from Webpack config but I’m not comfortable messing with Webpack. So if anyone has any suggestions and example, I’m happy to listen.
So I go back to the beginning. I chose CSS because I’m more familiar with it. This is so NOT ME. I should not be afraid of trying new things.
I started some research on CSS-in-JS. Well, it didn’t take long, because Material-UI core styling is using JSS. The dependency is built-in so there is no additional bundle size overhead. Furthermore, I have already used some JSS for overriding the MUI theme globally in the code. Although there might be more popular or powerful CSS-in-JS libraries, using JSS here should be the first choice.
Then I re-wrote the component:
// MyIcon.tsx
import * as React from 'react';
import IconButton from '@material-ui/core/IconButton';
import Close from '@material-ui/icons/Close';
import { makeStyles } from '@material-ui/core/styles';const useStyles = makeStyles({
root: {
padding: '0 0.5em',
color: 'purple',
minWidth: 0,
},
});const MyIcon: React.FunctionComponent = () => {
const classes = useStyles(); return (
<IconButton className={classes.root}>
<Close />
</IconButton>
);
};export default MyIcon;
That’s it. All in one file. And it works without !important
.
So let’s compare. I basically copy the stylesheet to .tsx
file. There is a bit of work (and a bit learning curve) but the idea is simple: 1) convert the CSS keywords from kebab-case to camelCase, 2) add a quote to CSS values, or make pixel value a number without “px”.
There are different ways to utilize JSS in the component but since I’m using Material-UI, I use makeStyles
to get a hook and then get the classes inside the functional component.
In short, I like my new choice: everything is JavaScript and is in one file (if you say the file will get too big, then I’ll argue you should break your component into smaller pieces); no more guessing game with !important
; no need to deal with CSS typing.
And more studies came
The above example code shows how to use JSS in React functional component, but it won’t work with the React class component, because makeStyles
generates a hook, which can only be called inside a functional component. So how to use JSS for the class component?
The code can be rewritten into this:
import * as React from 'react';
import IconButton from '@material-ui/core/IconButton';
import Close from '@material-ui/icons/Close';
import { withStyles, WithStyles } from '@material-ui/core/styles';const myStyles = () => {
return {
root: {
padding: '0 0.5em',
color: 'purple',
minWidth: 0,
},
};
};class MyIcon extends React.Component<WithStyles> {
constructor(props: WithStyles) {
super(props);
} public render() {
const { classes } = this.props; return (
<IconButton className={classes.root}>
<Close />
</IconButton>
);
}
}export default withStyles(myStyles)(MyIcon);
It is using MUI’s withStyles
instead. It is a HOC (Higher-Order Components) rendering custom component with custom styles. It’s not bad either!
So what else can I use in JSS? First I tried the pseudo-class. Instead of using MUI IconButton
, I just simply tried a div
:
import * as React from 'react';
import IconButton from '@material-ui/core/IconButton';
import Close from '@material-ui/icons/Close';
import { makeStyles } from '@material-ui/core/styles';const useStyles = makeStyles({
root: {
padding: '0 0.5em',
color: 'purple',
minWidth: 0,
'&:hover': {
cursor: 'pointer',
backgroundColor: 'orange',
},
},
});const MyIcon: React.FunctionComponent = () => {
const classes = useStyles(); return (
<div className={classes.root}>
<Close />
</div>
);
};export default MyIcon;
Voila! Next, nested class:
import * as React from 'react';
import IconButton from '@material-ui/core/IconButton';
import Close from '@material-ui/icons/Close';
import { makeStyles } from '@material-ui/core/styles';const useStyles = makeStyles({
root: {
padding: '0 0.5em',
color: 'purple',
minWidth: 0,
'& label': {
textTransform: 'uppercase',
},
},
});const MyIcon: React.FunctionComponent = () => {
const classes = useStyles();return (
<IconButton className={classes.root}>
<label>close</label>
<Close />
</IconButton>
);
};export default MyIcon;
Yep, also straightforward.
Any comments or suggestions are welcome! Peace out!