# Migration to emotion ## Converting styles from JSS to emotion Things to consider when migrating component's styles to emotion. In v4 we were defining the styles like: ```javascript= export const styles = (theme) => ({ /* Styles applied to the root element. */ root: {}, /* Styles applied to the root element if `container={true}`. */ container: { boxSizing: 'border-box', display: 'flex', flexWrap: 'wrap', width: '100%', }, /* Styles applied to the root element if `item={true}`. */ item: { boxSizing: 'border-box', margin: 0, // For instance, it's useful when used with a `figure` element. }, // ... } ``` In v5, we are using the `styled()` (named `experimentalStyled()` for now) utility for creating the components. This means, that we no longer have keys for specifying styles for specific props or slots, so we need to convert these keys to conditional spreading based on prop values, if they are based on a prop, or to completely different styled-components, if they are slots. The previous example would look something like this: ```javascript= const GridRoot = experimentalStyled( 'div', {}, { name: 'MuiGrid', slot: 'Root', overridesResolver, // we'll explain this part later }, )(({ theme, styleProps}) => ({ ...(stylesProps.container && { // apply these styles if container={true} boxSizing: 'border-box', display: 'flex', flexWrap: 'wrap', width: '100%', }), ...(stylesProps.item && { // apply these styles if item={true} boxSizing: 'border-box', margin: 0, // For instance, it's useful when used with a `figure` element. }) })); ``` If there are some slots, it would look like this: ```javascript= // Button styles const styles = (theme) => ({ root: { minWidth: 64, padding: '6px 16px', }, label: { display: 'flex', } }) ``` ```javascript= const ButtonRoot = experimentalStyled( 'div', {}, { name: 'MuiButton', slot: 'Root', overridesResolver, // we'll explain this part later }, )({ minWidth: 64, padding: '6px 16px', }); const ButtonLabel = experimentalStyled( 'div', {}, { name: 'MuiButton', slot: 'Label', }, )({ display: 'flex', }); ``` Note that we are using the `styleProps` for accessing the props value. This is done for mainly two reasons: - having one prop that is a collection of all related styling props, helps us in terms of perf to decide more easily which props should we spread on the HTML element and which should be ignored. - having a separate prop for this, can help us easily extend it to contain state as well as some derivative props which are calculated based on other props. ### Some "Got ya"-s - By defining the styles in one object, you are basically spreading all styles inside the style function result, which means that if you define the same key multiple times, it will override what you had defined before. This is usually what you want, but can be tricky when it comes to pseudo or class selector, breakpoints, etc. For example, spreading `':hover': {}` somewhere along the object will replace all definition for the `':hover'` styles you had before. To avoid this issue, we recommend you either spread all styles for the specific selector in one place, by moving the condition inside that definition or use multiple callbacks when defining the styles. You can see an example of the first one on the [Button component]( https://github.com/mui-org/material-ui/blob/next/packages/material-ui/src/Button/Button.js#L96) and for the second one on the [Container component](https://github.com/mui-org/material-ui/blob/next/packages/material-ui/src/Container/Container.js#L60) - If you found throughout the styles selectors like `$item`, you will need to replace them with a class (usually some of the classes available on the componentClasses object we export - more on this later). For example: ```diff= -'& > $item': { +[`& > .${gridClasses.item}`]: { maxWidth: 'none', }, ``` ## `experimentalStyled()` params I've mentioned that we are using the `experimentalStyled()`, now we will see what params we need to provide for each component that we create. The first parameter of the `experimentalStyled()` is the element or component that should serve as a base for the component. It can be a simple HTML element, like `div`, or a different component. For example, the `ButtonRoot` component is defined on top of the `ButtonBase`. The second parameter are additional option props for the `styled()` utility that comes from the `@material-ui/styled-engine` (emotion or styled-components), like `label`, `shouldForwardProp`, `target`. Mui is setting this by default for you, but if you need to opt-out you can do it. Finally, the third argument is the `overrideResolver` function, which based on the props and the style overrides coming from the theme, will create additional styles that will be applied on top of the default component styles. Here is one example of how the function can look like: ```javascript= const overridesResolver = (props, styles) => { const { styleProps } = props; return deepmerge(styles.root || {}, { ...styles[styleProps.variant], ...styles[`${styleProps.variant}${capitalize(styleProps.color)}`], ...styles[`size${capitalize(styleProps.size)}`], ...styles[`${styleProps.variant}Size${capitalize(styleProps.size)}`], ...(styleProps.color === 'inherit' && styles.colorInherit), ...(styleProps.disableElevation && styles.disableElevation), ...(styleProps.fullWidth && styles.fullWidth), [`& .${buttonClasses.label}`]: styles.label, [`& .${buttonClasses.startIcon}`]: { ...styles.startIcon, ...styles[`iconSize${capitalize(styleProps.size)}`], }, [`& .${buttonClasses.endIcon}`]: { ...styles.endIcon, ...styles[`iconSize${capitalize(styleProps.size)}`], }, }); }; ``` You will see here that, based on the values inside the `styleProps` we decide which overrides we need to apply, for example, if the `styleProps.disableElevation` is `true`, we will apply the `styles.disableElevation` overrides (line 10 on the example above). For the slots, we need to use the class selector for the appropriate slot when adding the overrides, we have in the example above overrides for the `label`, `startIcon` and `endIcon` slots accordingly. ## Utility & override classes We expect each component to export as part of its package object containing the default utility classes as well as a helper function for generating utility classes. For creating this you should use the `generateUtilityClass` and `generateUtilityClasses` helpers from `@mateiral-ui/unstyled`. Here is an example of this: ```javascript= import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled'; export function getButtonUtilityClass(slot) { return generateUtilityClass('MuiButton', slot); } const buttonClasses = generateUtilityClasses('MuiButton', [ 'root', 'label', 'text', 'textInherit', 'textPrimary', 'textSecondary', 'outlined', 'outlinedInherit', 'outlinedPrimary', 'outlinedSecondary', // ... ]); export default buttonClasses; ``` These utility classes are used later in the tests to ensure that the logic of the component is correct. Also, they are useful when defining the styles if any of the utility classes should be used as selectors. In the component's logic, you need to actually add these classes to the rendered tree based on the `styleProps`. We do this by defining a new `useUtilityClasses` hook. Here is an example of it: ```javascript= const useUtilityClasses = (styleProps) => { const { color, disableElevation, fullWidth, size, variant, classes = {} } = styleProps; // Define here key for each of the slots of the component const slots = { root: [ 'root', variant, `${variant}${capitalize(color)}`, `size${capitalize(size)}`, `${variant}Size${capitalize(size)}`, color === 'inherit' && 'colorInherit', // some classes may be added conditionally based on a prop disableElevation && 'disableElevation', fullWidth && 'fullWidth', ], label: ['label'], startIcon: ['startIcon', `iconSize${capitalize(size)}`], endIcon: ['endIcon', `iconSize${capitalize(size)}`], }; // always use this utility for generating the classes for the slots // it will make sure that both the utility and props' classes will be applied return composeClasses({ slots, classes, getUtilityClass: getButtonUtilityClass }); }; ``` When you use this inside the component, make sure that you invoke it with the `styleProps` and make sure that those contain the props' `classes`, so that overrides will be applied. ## Theme default props You need to make sure you use the `unstable_useThemeProps` hook from `@material-ui/core/styles`, so that theme's default props will be merged with user-defined. Usually each component's render method, will start with using this hook: ```javascript= const Grid = React.forwardRef(function Grid(inProps, ref) { const props = useThemeProps({ props: inProps, name: 'MuiGrid' }); // ... } ``` You need to make sure that you will use the correct name here, as it is used as a key in the theme's components definition. In addition to the component's props, this hook returns also the `isRtl` and `theme` props, which can be accessed inside the rendered tree. ## How to use new the `ComponentRoot` & other slot components Each component that you create with the `experimentalStyled()` utility should be used somehow in your render tree. The `ComponentRoot` should be returned as the root of your rendered tree and it usually requires the `as` property set to the `component` prop if available in the core component. You need to make sure you also provide the `stylesProps` as well as the `classes['slot']` which we generated with the `useUtilityClasses` hook. ## TS updates All components that are created using `experimentalStyled()`, have the support for the `sx` prop. Hence, this prop needs to be added to the components' props. ## Test updates For the test to work as expected, you need to first import the `componentClasses` and consider them as `classes` throughout all test suites, instead of generating them with `getClasses()`. In addition to this, you need to convert the `describeConformance` to `describreConformanceV5`. You will most likely need to add some additional options there. For this, I recommend following what is done in the `Button.test.js`. There may be required some additional updates on the tests, based on how the generated styles are being tested, but this is a per-component basis. ## Potential issues and their fixes If `yarn workspace framer build` is failing, you will need to add the new `sx` prop in the `ignoredProps` for the compoennt you are migrating. For example: ```diff= diff --git a/framer/scripts/framerConfig.js b/framer/scripts/framerConfig.js index fb02f439b6..526d4d294a 100644 --- a/framer/scripts/framerConfig.js +++ b/framer/scripts/framerConfig.js @@ -225,6 +225,7 @@ export const componentSettings = { }, Paper: { ignoredProps: [ + 'sx', // FIXME: `Union` 'variant', ], ```