Material UI v4 makeStyles exported from a single file doesn't retain the styles on refresh

I am using Material UI v4, i'm exporting my styles from a single file, But the styles won't work in other components styles.js

const useStyles = makeStyles(theme => ({
    root: {
      display: 'flex',
    },
    // textField component styles
    textFieldInput: {
      margin: theme.spacing(2),
      width: 250,
      minWidth: 250,
    },
    formControl: {
      margin: theme.spacing(2),
      minWidth: 120,
    },


})
export {useStyles}

In my component file

    ....
    const classes = useStyles(styles);

    return (
        <TextField
            className={classes.textFieldInput}
            label={label}
            placeholder={label}
            error={touched && invalid}
            helperText={touched && error}
            {...input}
            disabled={disabled || false}
            readOnly={readOnly || false}
            required={required || false}
            InputProps={{ readOnly, ...custom }}
            {...custom}
        />
    );
     ....

when i use it in my components the styles will work on the first hot reload but after that the styles wont have any effect any ideas why? and how i can fix this


Solution 1:

Why is this happening?

If you have two CSS classes applied to the same element with the same degree of specificity, then the winner will be the CSS class that is defined last within the document (based on the order of the <style> elements in the <head>, NOT the order of the class name strings in the class attribute of the element being styled).

This page is an example with two TextField elements that reproduces your problem. If you open the browser developer tools and look at the <style> elements, you will see that the styles from makeStyles come first followed by the styles from TextField (e.g. MuiFormControl). I've shown an abbreviated version below:

<style data-jss="" data-meta="makeStyles">
.makeStyles-textFieldInput-1 {
  margin: 32px;
  min-width: 250px;
}
</style>
<style data-jss="" data-meta="MuiFormControl">
.MuiFormControl-root {
  border: 0;
  margin: 0;
  display: inline-flex;
  padding: 0;
  position: relative;
  min-width: 0;
  flex-direction: column;
  vertical-align: top;
}
.MuiFormControl-marginNormal {
  margin-top: 16px;
  margin-bottom: 8px;
}
.MuiFormControl-marginDense {
  margin-top: 8px;
  margin-bottom: 4px;
}
.MuiFormControl-fullWidth {
  width: 100%;
}
</style>
<style data-jss="" data-meta="MuiTextField">

</style>

The MuiFormControl-root class is applied to the same element as the class specified via TextField's className property (e.g. the textFieldInput class from makeStyles/useStyles). Since the MuiFormControl <style> element occurs after the makeStyles <style> element, MuiFormControl's default styling for margin and min-width win over the custom styling specified by makeStyles.

The order of these <style> elements is controlled by the order in which makeStyles is called. For the default styling for a given Material-UI component, makeStyles is called at the point when the component is first imported.

For typical usage patterns, where makeStyles is called in the same JavaScript file that then calls useStyles and passes the classes to the Material-UI component, the order will always be what you would want because the imports of the Material-UI components will happen before the call to makeStyles.

When you move the call to makeStyles to a separate file and import the useStyles method that it returns, you introduce the possibility of importing useStyles before importing the Material-UI component (e.g. TextField in this case).

This is demonstrated in the code in this sandbox: https://codesandbox.io/s/makestyles-first-i1mwh

The reason it may work on the first hot reload is that the makeStyles <style> element is being removed and then added on to the end when you make changes. The Mui* style elements don't change so they remain where they are (which is before the new makeStyles style element until the page is reloaded).

You can't easily shoot yourself in the foot this way using the Higher-order component API (i.e. withStyles) since makeStyles is called within withStyles so you will have always imported the component being wrapped by withStyles before passing it as a parameter.


How can I fix this?

There are a few ways you could address this. One way is to just ensure that you import your useStyles function after importing Material-UI components such as TextField.

Change:

import { useStyles } from "./styles";
import TextField from "@material-ui/core/TextField";

to instead be:

import TextField from "@material-ui/core/TextField";
import { useStyles } from "./styles";

This is demonstrated here: https://codesandbox.io/s/import-textfield-first-9qybd

This is fairly brittle however if you have styles for more than one type of component in styles.js and import styles.js from many files, since then for it to work reliably you are dependent on importing all of the Material-UI components that are styled by styles.js before the first place that you import styles.js.


Another way to address this is to export styled versions of the Material-UI components instead of exporting the useStyles function. Then you just import this customized component instead of the Material-UI component.

import { withStyles } from "@material-ui/core/styles";
import MuiTextField from "@material-ui/core/TextField";

const styles = theme => ({
  root: {
    margin: theme.spacing(4),
    minWidth: 250
  }
});

export const TextField = withStyles(styles)(MuiTextField);

This is demonstrated with a couple different syntax options here: https://codesandbox.io/s/import-styled-textfield-1ytxl

Solution 2:

I have been facing a similar problem and managed to solve it as below

root: {
        '&&': {
            width: "128px",
            height: "128px",
            margin: "8px",
        }
    },

I later also found an article about this here.

Using the double ampersands "&&" increases/doubles up the specificity/priority. So, for any class I wanted to override, I added the '&&' in my declared class.

This solved the problem for me without any noticeable downside but I don't know if this is actually a good practice. If anyone knows more about why not to use this, please let me know.