LoginSignup
7
4

More than 3 years have passed since last update.

TypeScriptでMaterial-UIコンポーネントをカスタマイズして再利用する

Last updated at Posted at 2019-10-24

動機

Material-UI (MUI) のTypographyにいちいちparagraph属性を書くのが面倒なので、書かなくていいようにしたかった。

実例リポジトリ

方法1:テーマでコンポーネントのデフォルトプロパティを設定する

コメントでkisaragiさんにで教えていただきました。MUIにはテーマでコンポーネントのデフォルトプロパティを設定する機能があり、そこでparagraph: trueとするとテーマ配下のコンポーネント全体にプロパティが適用されます。

次の例ではテーマオブジェクトに{props: { MuiTypography: { ... }}を設定しています。propsがデフォルトプロパティ設定を表すキー、MuiTypographyがコンポーネントのスタイル名を表すキーです。このスタイル名はAPIドキュメンテーションのCSSセクションか、直接ソースを参照して調べます。

index.tsx
const theme = createMuiTheme({
  palette: {
    primary: cyan,
  },
  props: {
    MuiTypography: {
      paragraph: true,
    },
  },
})

ReactDOM.render(
  <MuiThemeProvider theme={theme}>
    <App />
  </MuiThemeProvider>,
  document.getElementById('root')
)

この場合、テーマ配下のコンポーネントでTypographyで元々の挙動にしたい場合にはparagraph={false}を与える必要が生じます。影響範囲を限定したい場合にはテーマをネストさせます。

index.tsx
const parentTheme = createMuiTheme({
  palette: {
    primary: cyan,
  },
})

const childTheme = createMuiTheme({
  props: {
    MuiTypography: {
      paragraph: true,
    },
  },
})

ReactDOM.render(
  <MuiThemeProvider theme={parentTheme}>
    <Typography>親テーマの配下</Typography>
    <MuiThemeProvider theme={childTheme}>
      <Typography>親テーマと子テーマの配下</Typography>
    </MuiThemeProvider>,
  </MuiThemeProvider>,
  document.getElementById('root')
)

方法2:プロパティ設定済みのコンポーネントを作る

デフォルトの挙動も残して区別するなら、コンポーネントを定義して別名を与えるのもいいでしょう。props.childrenを受け取ってTypographyコンポーネントを返すシンプルなコンポーネントを作ってみます。

src/ts/Para.tsx
import * as React from 'react'
import Typography, {TypographyProps} from '@material-ui/core/Typography'

const Para = ({children, ...other}: TypographyProps): JSX.Element {
  return (
    <Typography paragraph {...other}>{children}</Typography>
  )
}

export default Para

TypeScriptなのでプロパティの型を書いています。TypeScriptでReactを書くとき面倒なポイントのひとつですが、ここでは@material-ui/core/TypographyパッケージからTypographyPropsをインポートして使います。

Typographyコンポーネントのプロパティ型はTypographyPropsインターフェース」というように、MUIではコンポーネント名の末尾にPropsを追加したインターフェースが提供されているので、今回のようにMUIのコンポーネントを拡張したい場合に便利です。

<aside>
ただしTypographyに関しては罠があって、@material-ui/systemTypographyPropsというtypeをエクスポートしているんですが、こちらは別物。@material-ui/system/TypographyPropsMUIのコンポーネント一般に備わっているタイポグラフィAPIのプロパティ型で、Typographyコンポーネント用ではありません。VSCodeのサジェストを信じてうっかりこちらをインポートするとハマります(ハマりました)。まぎらわしい。
</aside>

さらにTypographyProps@material-ui/coreからはエクスポートされていません。@material-ui/core/Typographyから、きちんとコンポーネント単位でインポートする必要があります。まとめると以下。

// オッケー!
import Typography, {TypographyProps} from '@material-ui/core/Typography'
// ダメ!
import Typography from '@material-ui/core/Typography'
import {TypographyProps} from '@material-ui/system'
// ダメ!
import {Typography, TypographyProps} from '@material-ui/core'

別コンポーネントにする場合でも子テーマを使うこともできます。今回くらいのユースケースでは冗長な気もしますが、規模が大きくなると使いどころが出てくるかもしれません。

const childTheme = createMuiTheme({
  props: {
    MuiTypography: {
      paragraph: true,
    },
  },
})

const Para = ({children, ...other}: TypographyProps): JSX.Element (
  <MuiThemeProvider theme={childTheme}>
    <Typography {...other}>{children}</Typography>
  </MuiThemeProvider>
)

発展:スタイルを当てる

せっかくカスタマイズするのだからスタイルも設定したい。ということでそれぞれ方法を見てみます。

テーマでスタイルを設定

テーマオブジェクトではスタイルを設定することもできます。

overrideというキーの下にコンポーネントのスタイル名を指定し、さらにコンポーネントごとに決められたルール名を指定してスタイルを書きます。スタイル名とルール名はコンポーネントAPIドキュメンテーションのCSSセクションを参照します。プロパティ設定が特定条件を満たした場合のみ有効になるスタイルなど、柔軟な設定も可能です。

index.tsx
const theme = createMuiTheme({
  palette: {
    primary: cyan,
  },
  props: {
    MuiTypography: {
      paragraph: true,
    },
  },
  overrides: {
    MuiTypography: {
      root: {
        backgroundColor: 'lightblue',
      },
    },
  },
})

ReactDOM.render(
  <MuiThemeProvider theme={theme}>
    <App />
  </MuiThemeProvider>,
  document.getElementById('root')
)

MUIのmakeStyles()やstyled()でスタイルを設定

@material-ui/core/stylesのAPIを使ってみます。次の例ではmakeStyles()を使ったParaMuiと、styled()を使ったParaStyledというコンポーネントを作っています。

src/ts/Para.tsx
import * as React from 'react'
import Typography, {TypographyProps} from '@material-ui/core/Typography'
import {
  Theme,
  createStyles,
  makeStyles,
  styled,
} from '@material-ui/core/styles'

const styles = {
  marginBottom: '1rem',
  backgroundColor: '#FFAAAA',
  '&:last-child': {
    marginBottom: 0,
    backgroundColor: '#CCCCFF',
  },
}

const useStyles = makeStyles((theme: Theme) => createStyles({
  para: {
    ...styles,
    marginBottom: theme.spacing(4),
    backgroundColor: theme.palette.primary.main,
    color: theme.palette.primary.contrastText,
  },
}))

// with MUI
export const ParaMui = ({children, className, ...other}: TypographyProps) => {
  const classes = useStyles()
  return (
    <Typography className={`${className} ${classes.para}`} {...other}>
      {children}
    </Typography>
  )
}

// with styled-components API
const ParaStyledBase = ({children, ...other}: TypographyProps) => (
  <Typography {...other}>{children}</Typography>
)

export const ParaStyled = styled(ParaStyledBase)(styles)

プレーンな(型を指定しない)オブジェクトスタイルのJSSを用意し、makeStyles()を使ったParaMuiではMUIのテーマAPIを使って上書きしつつ、useStylesフック関数を取得しています。

そのフック関数をParaMuiコンポーネントで呼び、クラス名を持つオブジェクトclassesを得て、TypographyコンポーネントのclassNameに指定しています。今回はテンプレートリテラルでクラス名をつなぎましたが、MUIがclsxに依存しているので、実環境ではclsxを使うところです。

styledAPIを使ったParaStyledは、最初に作ったstylesオブジェクトを渡すだけでスタイルが当たり、classNameプロパティを書く必要もなく、とてもシンプルです。テンプレートリテラルでCSSを書きたい場合などにstyled-componentsEmotionへ移行するのもスムーズでしょう(MUIでもJSSプラグインでテンプレートリテラルを使えるようにできますが、セレクタやネストが使えない制限があるようです)。

ただMUIのデフォルトテーマやMuiThemeProviderコンポーネントで与えたテーマを使おうとする(theme.spacing()theme.paletteなど)と、makeStylesでやったようなJSSオブジェクト内での利用はできず、コンポーネント内でどうにかする(useTheme()フックでThemeを受け取る)しかありません。MUIとの統合を重視するとイマイチな印象です。JSS内でテーマはいらないと割り切れるなら、シンプルに書ける魅力はあります。

<aside>
withStyles()classesプロパティをコンポーネントに渡すだけなのでmakeStylesと同じことになり、styled()のwithThemeオプションwithTheme()themeプロパティをコンポーネントに渡すものなので、コンポーネント内でカスタマイズすることになります。デフォルトテーマとの統合を重視する場合には、makeStyles((theme: Theme) => {...})でテーマを受け取っておいて、もしコンポーネント内でもテーマを使いたいならさらにuseTheme()フックを使うのが素直に思えるんですが、好みもあるところでしょう。styledスタイルでMUIテーマを利用する良いアイデアがあれば教えてください。
</aside>

使用バージョン

詳しくは実例リポジトリのpackage.jsonyarn.lockを参照してください。

@material-ui/core 4.5.1
@types/react 16.9.9
@types/react-dom 16.9.2
parcel-bundler 1.12.4
pug 2.0.4
react 16.11.0
react-dom 16.11.0
typeface-roboto 0.0.75
typescript 3.6.4
7
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4