DEV Community

suvo
suvo

Posted on • Edited on

Upgrade material ui 4 makeStyles and withStyles to material ui 5 using emotion JS with TS

I just came across a very nice blog when the upgrading to mui5 using emotion, it is nicely demonstrated here
But there are a few things which are lacking in this implementation, i.e. TS support, how to handle withStyles Styled components.
In this blog post I would mention those missing items.

Styles Root part is same as mentioned in the above mentioned blog.

The emotion theme declaration

import { Theme as MuiTheme } from '@mui/material/styles'
import '@emotion/react'

declare module '@emotion/react' {
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
  export interface Theme extends MuiTheme {}
}
Enter fullscreen mode Exit fullscreen mode

Custom hook with TS support

import { useMemo } from 'react'
import { css, CSSInterpolation } from '@emotion/css'
import { useTheme } from '@emotion/react'
import { Theme as MuiTheme } from '@mui/material/styles'

function useEmotionStyles(
  styles: () => Record<string, CSSInterpolation>
): Record<string, ReturnType<typeof css>>

function useEmotionStyles(
  styles: (theme: MuiTheme) => Record<string, CSSInterpolation>
): Record<string, ReturnType<typeof css>>

function useEmotionStyles<T>(
  styles: (theme: MuiTheme, props: T) => Record<string, CSSInterpolation>,
  props: T
): Record<string, ReturnType<typeof css>>

function useEmotionStyles<T>(
  styles: (theme: MuiTheme, props?: T) => Record<string, CSSInterpolation>,
  props?: T
): Record<string, ReturnType<typeof css>> {
  const theme = useTheme()
  return useMemo(() => {
    const classes = styles(theme, props)
    const classNameMap = {}

    Object.entries(classes).forEach(([key, value]) => {
      classNameMap[key] = css(value)
    })

    return classNameMap
  }, [props, styles, theme])
}

export default useEmotionStyles
Enter fullscreen mode Exit fullscreen mode

Here we have a overloaded hook for possible calls to the hook.

A simple example would be

type GridProps = { itemMargin: number | string }

const gridStyles = (theme: Theme, { itemMargin }: GridProps) => ({
  container: {
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    flexWrap: 'wrap' as CSSTypes.Property.FlexWrap,
    maxWidth: theme.breakpoints.values.md,
    [theme.breakpoints.down('sm')]: {
      maxWidth: 420
    },
    '&>*': {
      margin: itemMargin
    }
  }
})


const Component = () => {
  const { container } = useEmotionStyles<GridProps>(gridStyles, { itemMargin })

  return (
    <Container className={container}>
  )
}
Enter fullscreen mode Exit fullscreen mode

If you want to implement keyframes animations using emotion you can use this way.

import { css, keyframes } from '@emotion/react'

const fadeIn = keyframes({
  '0%': {
    opacity: 0
  },
  '100%': {
    opacity: 1
  }
})
const styles = () => ({
  text: css({
    display: 'flex',
    alignItems: 'center',
    animation: `${fadeIn} 2s`
  })
})

Enter fullscreen mode Exit fullscreen mode

Custom Hook for Styled Component (replacement for withStyles)

import React, { useMemo } from 'react'
import { Theme, useTheme } from '@emotion/react'
import { Theme as MuiTheme } from '@mui/material/styles'
import styled, { StyledComponent } from '@emotion/styled/macro'
import { CSSInterpolation } from '@emotion/css'
import {
  OverridableComponent,
  OverridableTypeMap
} from '@mui/material/OverridableComponent'

type ReturnedType<T extends ComponentType> = StyledComponent<
  JSX.LibraryManagedAttributes<T, React.ComponentProps<T>> & {
    theme?: Theme
  }
>

type ComponentType =
  | OverridableComponent<OverridableTypeMap>
  | React.JSXElementConstructor<JSX.Element>
  | ((props?: React.ComponentProps<any>) => JSX.Element)

function useEmotionStyledComponent<T extends ComponentType>(
  styles: () => Record<string, CSSInterpolation>,
  WrappedComponent: T
): ReturnedType<T>

function useEmotionStyledComponent<T extends ComponentType>(
  styles: (theme: MuiTheme) => Record<string, CSSInterpolation>,
  WrappedComponent: T
): ReturnedType<T>

function useEmotionStyledComponent<T extends ComponentType, R>(
  styles: (theme: MuiTheme, props: R) => Record<string, CSSInterpolation>,
  WrappedComponent: T,
  props: R
): ReturnedType<T>

function useEmotionStyledComponent<T extends ComponentType, R>(
  styles: (theme: MuiTheme, props?: R) => Record<string, CSSInterpolation>,
  WrappedComponent: T,
  props?: R
): ReturnedType<T> {
  const theme = useTheme()
  return useMemo(() => {
    const strings = styles(theme, props)

    return styled(WrappedComponent)(strings?.root)
  }, [WrappedComponent, props, styles, theme])
}

export default useEmotionStyledComponent

Enter fullscreen mode Exit fullscreen mode

To use this hook there must be only one root element in styles and all styles must be inside it.

const StyledDialog = (props: DialogProps) => {
  const Component = useEmotionStyledComponent<typeof Dialog>(
    (theme: Theme) => ({
      root: {
        '& div.MuiDialog-container': {
          height: 'auto'
        },
        '& div.MuiDialog-paper': {
          alignItems: 'center',
          padding: theme.spacing(0, 2, 2, 2),
          minWidth: 240
        }
      }
    }),
    Dialog
  )
  return <Component {...props} />
}

const MenuButton = (props: FabProps) => {
  const StyledMenuButton = useEmotionStyledComponent<typeof Fab, FabProps>(
    (theme: Theme) => ({
      root: {
        position: 'fixed',
        top: theme.spacing(2),
        left: theme.spacing(4)
      }
    }),
    Fab,
    props
  )
  return <StyledMenuButton {...props} />
}
Enter fullscreen mode Exit fullscreen mode

and use this component as a Styled component.

With this two custom hooks you can replace the makeStyles and withStyles, if you have any questions, let me know.

Top comments (0)