Material-ui: [styles] How to merge multiple styles?

Created on 21 May 2018  路  25Comments  路  Source: mui-org/material-ui

Material-ui uses the classes object to pass custom styles to component. withStyles helps us process theming and merging our customizations with the top level styles. Material-ui uses common classnames such as root in multiple components. This is how I understand it.

Often we have multiple components on a page and I don't understand what the best way is to use withStyles in this context if I for instance want to adjust the root style for multiple components?

  • [x ] This is a v1.x issue (v0.x is no longer maintained).
  • [ x] I have searched the issues of this repository and believe that this is not a duplicate.

Expected Behavior

Would it be possible to comment on the strategy that you suggest we use for this case and possibly add documentation?

Current Behavior

There is no documentation on this topic.

Your Environment

| Tech | Version |
|--------------|---------|
| Material-UI | v1.0.0 |
| React |16.3 |

enhancement styles

Most helpful comment

I made a utility to make this a lot easier:

function combineStyles(...styles) {
  return function CombineStyles(theme) {
    const outStyles = styles.map((arg) => {
      // Apply the "theme" object for style functions.
      if (typeof arg === 'function') {
        return arg(theme);
      }
      // Objects need no change.
      return arg;
    });

    return outStyles.reduce((acc, val) => Object.assign(acc, val));
  };
}

export default combineStyles;

And I use like this:

// Import the utility function above ^
import combineStyles from 'path/to/combineStyles'

// Now we can import contextual styles where we need them (preferred):
import buttonStyles from '/path/to/buttonStyle';
import componentStyles from '/path/to/componentStyle';

// We can use style functions that make use of the theme (example):
const s1 = theme => ({
  toolbar: {
    backgroundColor: theme.palette.primary.main,
    color: '#fff',
    ...theme.mixins.toolbar,
  },
  link: {
    color: theme.palette.primary.main,
    width: '100%',
    textDecoration: 'none',
    padding: '12px 16px',
  },
});

// And we can use style objects (example):
const s2 = {
  menuItem: {
    height: 'auto',
    padding: 0,
  },
};

// Use our util to create a compatible function for `withStyles`:
const combinedStyles = combineStyles(s1, s2, buttonStyles, componentStyles);

// And use `withStyles` as you would normally:
export default withStyles(combinedStyles)(MyComponent);

Unless I'm mistaken, I would have thought withStyles would have _already_ worked with multiple styles out of the box, but if not, it would be cool to see an official wrapper like the one above.

All 25 comments

The className property is the simplest way to override the root component's styles:

https://material-ui.com/customization/overrides/#overriding-with-class-names

If you are using the classes prop to override multiple styles of multiple components, then simply name them differently, and apply them to the appropriate components.

Here's a recently merged example (same component, different styles):
https://github.com/mbrookes/material-ui/blob/0b7e2212aa37ce2157436c2f5ef6d8b67c1c915c/docs/src/pages/demos/selection-controls/CustomizedSwitches.js

Right, thanks for that. I'm trying to grok how theming might (or not) apply if I would use different classNames than the defined ones in the styles object that I pass to withStyles. In the example, would classes.iOSSwitchBase override the switchBase property or merge with it?

I guess if it merges it would make sense to me. If it overrides I would want to know how to mix the default with the override

Often we have multiple components on a page and I don't understand what the best way is to use withStyles in this context if I for instance want to adjust the root style for multiple components?

As far as I understand your problem, this documentation section is here to provide guidance on the very topic you raised: https://material-ui.com/customization/overrides/.

Personally, the strategy I'm using is:

  • Never using theme.overrides as it's making code splitting impossible
  • Wrapping all the components I need to consistently change
  • Using className > classes where possible

@mschipperheyn Does that answer your problem?

I guess it does. BTW, awesome library! Helps me be very productive

I recently ran into this problem when using withStyles, I was able to get around it with the following

  withStyles(
    (theme) => ({
      ...globalStyles(theme),
      ...tableStyles(theme),
    }),
    { withTheme: true },
  ),

I made a utility to make this a lot easier:

function combineStyles(...styles) {
  return function CombineStyles(theme) {
    const outStyles = styles.map((arg) => {
      // Apply the "theme" object for style functions.
      if (typeof arg === 'function') {
        return arg(theme);
      }
      // Objects need no change.
      return arg;
    });

    return outStyles.reduce((acc, val) => Object.assign(acc, val));
  };
}

export default combineStyles;

And I use like this:

// Import the utility function above ^
import combineStyles from 'path/to/combineStyles'

// Now we can import contextual styles where we need them (preferred):
import buttonStyles from '/path/to/buttonStyle';
import componentStyles from '/path/to/componentStyle';

// We can use style functions that make use of the theme (example):
const s1 = theme => ({
  toolbar: {
    backgroundColor: theme.palette.primary.main,
    color: '#fff',
    ...theme.mixins.toolbar,
  },
  link: {
    color: theme.palette.primary.main,
    width: '100%',
    textDecoration: 'none',
    padding: '12px 16px',
  },
});

// And we can use style objects (example):
const s2 = {
  menuItem: {
    height: 'auto',
    padding: 0,
  },
};

// Use our util to create a compatible function for `withStyles`:
const combinedStyles = combineStyles(s1, s2, buttonStyles, componentStyles);

// And use `withStyles` as you would normally:
export default withStyles(combinedStyles)(MyComponent);

Unless I'm mistaken, I would have thought withStyles would have _already_ worked with multiple styles out of the box, but if not, it would be cool to see an official wrapper like the one above.

@dlochrie Thanks for sharing this utility function! I have renamed the issue to better match the content.
withStyles takes two arguments, that last one is for the options. I don't see how we could move this logic into the module, relying on a utility helper seems to be the best approach. Let's hear more feedback from our users before doing anything :).

Cool! Glad you like it. If the utility makes it into Material UI in any way, I'm sure the community can help to improve it as well :smile:

I encountered this problem when trying to apply a sort of "DRY" implementation for my styles. I created a styles JS file that basically looked like this.

export const defaultPageStyle = theme => ({
    root: {
        flexGrow: 1,
        marginTop: theme.mixins.toolbar.minHeight,
        backgroundColor: theme.palette.background.default,
    },
})

Then in my components I still applyed the styles template to add style to elements in the local component. which looked like this (as we all know)

import { withStyles } from '@material-ui/core/styles';

const styles = theme => ({
  gridContainer : {
    padding: theme.spacing.unit *2
  }
})

class SomeComponent extends React.Component {
    ** some code **
}

The problem now was on how to merge on
export default withStyle(???)(SomeComponent)

Im not sure if anyone else is doing this but my workaround was to implement classnames on the styles of my component which looked like this.

const styles = theme => (classNames({
  gridContainer : {
    padding: theme.spacing.unit *2
  }
}, defaultPageStyles))

** component code ** 

export default withStyles(styles)(SomeComponent);

and it actually doesnt work -_-

EDIT:
Sorry, I didnt see @whitneyit solution and it solved my problem. But i probably wont delete this in case anyone encounters the same problem as I did. And if they do, they'd know @whitneyit's solution would fix it

@dlochrie have you considered adding your method to npm, so that it's more widely accessible, and can be tested by the community?

@pmgroney - yeah, it could be npm module. If I get a chance, I might package it and add some tests. In the meantime, I create a gist so that anyone can take and use as they want.

https://gist.github.com/dlochrie/8e962277e0b8da3bca8c5db72e5d95c2

@oliviertassinari It seems there's enough support to consider adding this to Material-UI...

@dlochrie Are you happy to work on this?

@mbrookes Sure, I can take a stab at it.

@dlochrie Correct me if i'm wrong but using your utility (And similar others shown here) on 2 different components will create 2 different classes even though the classnames are the same.
Meaning if i have a common style of

{ common: { color: 'red' } }

And then i use combineStyles on 2 different components, i'll end with 2 different common classNames in the DOM, no?

Edit:
The only solution i though of is to do something like

const commonStyles = { common: { color: 'red' } };

const commonWithStyles = withStyles(commonStyles, { name: 'common' });

const styles = { text: { fontSize: 10 } }

export default commonWithStyles(withStyles(styles)(Component))

or something like that, the things is that this way, it will throw warnings saying that its possible to only override the classes in styles.
But in general it will work because once commonWithStyles is created, the classnames will stay the same even if its used across multiple components

@slavab89 That's a good question. I haven't had a use case where I've had name collisions, so I haven't seen that issue. It throws warnings?

There are no name collisions because JSS generates a different className for every component, so you'll end up having 2 classNames like ComponentA-common-11 and ComponentB-common-22. But the styles themselves in each one of them will be the same.

Regarding the warnings, i was referring to the option that i suggested (commonWithStyles(withStyles(styles)(Component))). That shows a warning because you're trying to send a classes prop to Component with different object keys than what "style" holds.
That method will work and the common classNames will be the same, but the warning is still there.

Yeah, that makes sense. I think it's all about how withStyles is being used. While I did start with the utility I created to help merge common styles, I started to think about how I could package my components so that wouldn't rely on multiple styles. For example, if I used the same style for my headers on multiple component/pages and components, then I would think about how to make the header a common component that came with its own styles.

So rather than this:

myComponent.js

import Paper from '@material-ui/core/Paper';

// These styles are common.
import commonHeaderStyles from '/path/to/headerStyles';

// These styles are local.
import myComponentStyles from './myComponentStyles';

const MyComponent = (props) => {
  const { classes } = props;

  return (
    <div>
      <div className={classes.commonHeaderWrapper}>
        <h1 className={classes.commonHeader}>
          My Common Header
        </h1>
      </div>
      <Paper className={classes.myComponentPaper}>
        {/* My component content */}
      </Paper>
    </div>  
  );
}

const combinedStyles = combineStyles(commonHeaderStyles, myComponentStyles);

export default withStyles(combinedStyles)(MyComponent);

I can do this:

commonHeader.js

// These styles are local to this component.
import styles from './styles';

const CommonHeader = ({ classes, text }) => (
  <div className={classes.commonHeaderWrapper}>
    <h1 className={classes.commonHeader}>{ text }</h1>
  </div>
);

export default withStyles(styles)(CommonHeader);

myComponent.js

import Paper from '@material-ui/core/Paper';

// These styles are local to this component.
import styles from './styles';

const MyComponent = ({ classes }) => (
    <div>
      <CommonHeader text="My Common Header" />
      <Paper className={classes.myComponentSpecificPaperOverride}>
        {/* My component content */}
      </Paper>
    </div>  
  );
);

export default withStyles(styles)(MyComponent);

Since I've started doing this, I haven't had to use my utility :smile: Of course there might areas where it makes sense, but React makes it so easy to decouple pieces of components that it's been easy enough for my use cases.

How would one use the combineStyles function in conjunction with the makeStyles api?

makeStyles : https://material-ui.com/css-in-js/basics/#hook-api

( related to last comment : https://github.com/mui-org/material-ui/issues/14821 )

@learninglegend It should work identically. However, the proposed combineStyles method by @dlochrie only support the theme, it doesn't support the props object. We have this helper that supports the props object but doesn't support the theme:
https://github.com/mui-org/material-ui/blob/a74b0205cb54cae0c3e9216f9cccb131b74afa39/packages/material-ui-system/src/compose.js#L3-L13
I think that we should build this missing helper.

In typescript and using https://github.com/mui-org/material-ui/issues/11517#issuecomment-407509327 to work with common styles ( combined ) this worked for me

const st1 = (theme: Theme) => createStyles({
    leftIcon: {
        marginRight: theme.spacing.unit,
    },
});

const st2 = (theme: Theme) => createStyles({
    root: {
        width: "100%",
    },
    grow: {
        flexGrow: 1,
    },
});

type withStyleProps = keyof ReturnType<typeof st1> | keyof ReturnType<typeof st2>;

const styles = combineStyles(st1, st2);

interface IProps
{
  some: boolean;
}

type IPropsAll = WithStyles<withStyleProps> & IProps;

const Sample = withStyles<withStyleProps>(styles, { withTheme: true })(
    class extends MyComponent<IPropsAll> {

        public render() {
            const classes = this.props.classes;

            return (
                <div className={classes.root}>                    
                  <FaToolbox className={classes.leftIcon} /> Preferences
                </div>
            );
        }

    });

export default Sample;

I created an example here to show how to use combineStyles with either withStyles or makeStyles: https://codesandbox.io/embed/merge-multiple-styles-qow51. Thanks @dlochrie

An update, this issue is being resolved in v5 thanks to #22342 and the new @material-ui/styled-engine package. The new approach is to provide an array to merge the styles:

import * as React from "react";
import { experimentalStyled as styled } from "@material-ui/core/styles";

const Div = styled("div")([
  {
    backgroundColor: "blue",
    "&:hover": {
      backgroundColor: "red"
    }
  },
  {
    color: "red",
    "&:hover": {
      color: "blue"
    }
  }
]);

export default function App() {
  return <Div>Hello</Div>;
}

It deep merges the styles: https://codesandbox.io/s/still-butterfly-11ury?file=/src/App.tsx:0-359.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sjstebbins picture sjstebbins  路  71Comments

amcasey picture amcasey  路  70Comments

NonameSLdev picture NonameSLdev  路  56Comments

iceafish picture iceafish  路  62Comments

nathanmarks picture nathanmarks  路  100Comments