動機
Material-UI (MUI) のTypography
にいちいちparagraph
属性を書くのが面倒なので、書かなくていいようにしたかった。
実例リポジトリ
方法1:テーマでコンポーネントのデフォルトプロパティを設定する
コメントでkisaragiさんにで教えていただきました。MUIにはテーマでコンポーネントのデフォルトプロパティを設定する機能があり、そこでparagraph: true
とするとテーマ配下のコンポーネント全体にプロパティが適用されます。
次の例ではテーマオブジェクトに{props: { MuiTypography: { ... }}
を設定しています。props
がデフォルトプロパティ設定を表すキー、MuiTypography
がコンポーネントのスタイル名を表すキーです。このスタイル名はAPIドキュメンテーションのCSSセクションか、直接ソースを参照して調べます。
const theme = createMuiTheme({
palette: {
primary: cyan,
},
props: {
MuiTypography: {
paragraph: true,
},
},
})
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<App />
</MuiThemeProvider>,
document.getElementById('root')
)
この場合、テーマ配下のコンポーネントでTypography
で元々の挙動にしたい場合にはparagraph={false}
を与える必要が生じます。影響範囲を限定したい場合にはテーマをネストさせます。
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
コンポーネントを返すシンプルなコンポーネントを作ってみます。
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/system
もTypographyPropsというtypeをエクスポートしているんですが、こちらは別物。@material-ui/system/TypographyProps
はMUIのコンポーネント一般に備わっているタイポグラフィ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セクションを参照します。プロパティ設定が特定条件を満たした場合のみ有効になるスタイルなど、柔軟な設定も可能です。
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
というコンポーネントを作っています。
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を使うところです。
styled
APIを使ったParaStyled
は、最初に作ったstyles
オブジェクトを渡すだけでスタイルが当たり、className
プロパティを書く必要もなく、とてもシンプルです。テンプレートリテラルでCSSを書きたい場合などにstyled-componentsやEmotionへ移行するのもスムーズでしょう(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.jsonやyarn.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