React + Material-UI v3 では withStyles(styles)(Component)
という独自の記法でスタイル付き Components を生成します。
TypeScript で Material-UI のスタイル付き Components を記述する場合は、従来の JavaScript での記法とは多少異なるのですが、 TypeScript を使った記法については情報が少ないと思ったので覚え書きとして残しておきます。
Material-UI v4 について
2019年5月末に Material-UI v4 がリリースされました。記法が変わったので、v4 の記法は別記事にしています。
この先は Material-UI v3 の記法になります。予めインストールした Material-UI のバージョンを確認してから閲覧してください。
TypeScript + Material-UI v4 のスタイル付きコンポーネント作成ガイド
https://qiita.com/cieloazul310/items/3088207acaa15a94bd44
更新
- 2019/02/23:
React.FunctionComponent
をReact.FC
に書き換えました。 - 2019/02/23:
(props: Props) => {}
を({ classes }: Props) => {}
に書き換えました。 - 2019/06/04: Material-UI v4 に関する追記を行いました。
環境
create-react-app
で作成したディレクトリを前提にしています。
tsconfig.json
や tslint.json
の設定はここでは省きます。
TypeScript React App の作成
$ create-react-app my-ts-app --scripts-version=react-scripts-ts
$ cd my-ts-app
$ yarn add @material-ui/core @material-ui/icons
バージョン
- @material-ui/core @3.8.3
- react @16.7.0
- react-dom @16.7.0
- react-scripts-ts @3.1.0
ディレクトリ構成
.── src
└── index.tsx (entry point)
└── App.tsx
└── components
│ └── FirstComponent.tsx
│ └── SecondComponent.tsx
│ └── ThirdComponent.tsx
└── utils
└── withRoot.tsx
以上のようなディレクトリ構成を想定しています。
ここからは ./src/components/
内にファイルを作成していきます。
従来の JavaScript + Material-UI の記法
まず従来の JavaScript + Material-UI でスタイル付き Components を作成する方法を見ておきましょう。
import React from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
// styles を定義
// theme を使わない場合は関数ではなく object でもよい
const styles = theme => ({
root: {
textAlign: 'center'
},
paragraph: {
fontFamily: 'serif',
padding: theme.spacing.unit * 2
}
});
// Component を作成
const FirstComponent = ({ classes, title }) => (
<div className={classes.root}>
<p className={classes.paragraph}>
{ title || 'My First TS Component' }
</p>
</div>
);
// withStyles(styles)(Component) で export
export default withStyles(styles)(FirstComponent);
Material-UI v3 では、上記のように withStyles(styles)(Component)
でスタイリングされた Component を作成することを推奨しています。
https://material-ui.com/style/typography/#component
TypeScript の場合 (Stateless Components 編)
TypeScript で Material-UI のスタイル付き Components を作成する場合です。
基本的にはMaterial-UI の TypeScript ガイドに則っています。
まず State を持たない Stateless Components の作成を、
- Function Components (props あり)
- Function Components (props なし)
- React Component Class
の3通りのパターンで書きます。型をかっちりと書きたい人向けの書き方なので、煩雑に感じたら JavaScript 的な書き方をしてもいいと思います。
TypeScript における Components の書き方は React & Redux in TypeScript - Static Typing Guide を参考にしています。
共通するコード
ここから先のコードで共通するのは以下の型定義と関数のインポートです。
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import createStyles from '@material-ui/core/styles/createStyles';
@material-ui/core/styles/createMuiTheme
から Theme
という型定義を、
@material-ui/core/styles/withStyles
から WithStyles
と StyleRules
という型定義を、
@material-ui/core/styles/createStyles
という関数を createStyles
としてインポートします。
1. Function Components (props あり)
import * as React from 'react';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import createStyles from '@material-ui/core/styles/createStyles';
// TypeScript で書く場合の styles 定義方法
// theme を使わない場合は、 styles = createStyles(object) でもよい
const styles = (theme: Theme) : StyleRules => createStyles({
root: {
textAlign: 'center'
},
paragraph: {
fontFamily: 'serif',
padding: theme.spacing.unit * 2
}
});
// Component の Props を WithStyles<typeof styles> で拡張
interface Props extends WithStyles<typeof styles> {
title?: string;
}
// Component を定義
const FirstComponent: React.FC<Props> = ({ classes, title }: Props) => (
<div className={classes.root}>
<p className={classes.paragraph}>
{ title || 'My First TS Component' }
</p>
</div>
);
// withStyles(styles)(Component) で スタイリングした Component を export
export default withStyles(styles)(FirstComponent);
Function Components を TypeScript で書く場合 React.SFC
を使う解説が多いのですが、React.SFC
は廃止予定となっているので React.FC
を使いました。
JavaScript のコードとの違い
1. styles の定義に createStyles
という関数を使う (重要)
styles で返すオブジェクトに createStyles
という関数を適用します。
Components のスタイリングに theme を使う場合は、
const styles = (theme: Theme): StyleRules => createStyles({
root: {
fontSize: 18
},
header: {
backgroundColor: theme.palette.primary.main
}
});
theme を使わない場合は、
const styles: StyleRules = createStyles({
root: {
fontSize: 18
}
});
という書き方をします。
createStyles
関数を使うことで型拡大を防ぐことができるようです。
参考: Using createStyles to defeat type widening
2. styles の関数の引数 theme に型定義 Theme
を与える
引数 theme に型定義 Theme
を与えることで、エディタの補助機能が効くようになり、theme を使ったスタイリングが楽になります。
theme について詳しくは公式ドキュメントをお読みください。
3. 型定義 Props
を WithStyles<typeof styles>
で拡張する
props の型定義 Props
を WithStyles<typeof styles>
で拡張します。
interface Props extends WithStyles<typeof styles>
は以下の型定義 Props
と同等のものになります。
const styles = (theme: Theme): StyleRules => createStyles({
root: {},
header: {},
paragraph: {},
footer: {}
});
// Props extends WithStyles<typeof styles> は下と等しくなる
interface Props {
title?: string;
classes: {
root: string;
header: string;
paragraph: string;
footer: string;
};
}
WithStyles<typeof styles>
を用いることで、上の例のように styles に定義した classes
の Key
をいちいち書く必要がなくなります。
2. Function Components (props なし)
import * as React from 'react';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import createStyles from '@material-ui/core/styles/createStyles';
const styles = (theme: Theme) : StyleRules => createStyles({
root: {
textAlign: 'center'
},
paragraph: {
fontFamily: 'serif',
padding: theme.spacing.unit * 2
}
});
const FirstComponent: React.FC<WithStyles<typeof styles>> = ({ classes }: WithStyles<typeof styles>) => (
<div className={classes.root}>
<p className={classes.paragraph}>
My First TS Component
</p>
</div>
);
export default withStyles(styles)(FirstComponent);
本来、型定義 Props
を置くところに WithStyles<typeof styles>
と置きます。
従来の props なしの Components では React.FC<{}>
と書けばいいのですが、withStyles
を使ってスタイリングを行なう場合、 Props は props.classes
というプロパティを持つことになります。
そのため、型定義は Props
の代わりに WithStyles<typeof styles>
を置かなければなりません。
この記法が煩雑に感じるなら、以下のような方法もあります。
interface Props extends WithStyles<typeof styles> {
// empty
}
あるいは、
type Props = WithStyles<typeof styles>;
と書いた上で、 React.FC<Props>
としても良いです。
3. React Component Class
class を使って State を持たない Stateless Components を作る場合
import * as React from 'react';
import { Theme } from '@material-ui/core/styles/createMuiTheme';
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles';
import createStyles from '@material-ui/core/styles/createStyles';
const styles = (theme: Theme) : StyleRules => createStyles({
root: {
textAlign: 'center'
},
paragraph: {
fontFamily: 'serif',
padding: theme.spacing.unit * 2
}
});
interface Props extends WithStyles<typeof styles> {
title?: string;
}
// Component を定義: React.PureComponent<Props> で拡張する
class SecondComponent extends React.PureComponent<Props> {
public render() {
const { classes, title } = this.props;
return (
<div className={classes.root}>
<p className={classes.paragraph}>
{title || "My Second TS Component"}
</p>
</div>
);
}
}
export default withStyles(styles)(SecondComponent);
State を持たない Stateless Components の作成で React Component Class を使うことは少ないと思いますが、参考までに載せておきます。
Stateless Components の場合、React.Component
ではなく React.PureComponent
を使った方が良いみたいです。
TypeScript で Stateful Components を作成する
State を持つ Components の場合も特に変わりません。
import * as React from "react";
import Button from '@material-ui/core/Button';
import { Theme } from "@material-ui/core/styles/createMuiTheme";
import withStyles, {
WithStyles,
StyleRules
} from "@material-ui/core/styles/withStyles";
import createStyles from "@material-ui/core/styles/createStyles";
const styles = (theme: Theme): StyleRules =>
createStyles({
root: {
textAlign: "center"
},
header: {
backgroundColor: theme.palette.primary.main,
boxShadow: theme.shadows[2],
padding: theme.spacing.unit * 2,
},
counter: {
fontSize: 60
},
});
interface Props extends WithStyles<typeof styles> {
title?: string;
}
interface State {
readonly counter: number;
}
class ThirdComponent extends React.Component<Props, State> {
readonly state: State = {
counter: 0
};
private _onIncrement = () => {
this.setState(prevState => ({
counter: prevState.counter + 1
}));
};
public render() {
const { classes, title } = this.props;
const { counter } = this.state;
return (
<div className={classes.root}>
<div className={classes.header}>
{title || "My Third TS Component"}
</div>
<div>
<span className={classes.counter}>{counter}</span>
</div>
<div>
<Button variant="contained" color="primary" onClick={this._onIncrement}>
Increment
</Button>
</div>
</div>
);
}
}
export default withStyles(styles)(ThirdComponent);
readonly
, private
, public
といった修飾子は任意でつけてください。
constructor
を書かない記法、メソッドではなくアロー関数でイベントハンドラを記述する記法は、先述の React & Redux in TypeScript - Static Typing Guide に則っています。
App に MuiTheme を適用する
ここまでは Material-UI + TypeScript におけるスタイル付き Components の作成方法を見てきました。 TypeScript 特有の記法の説明はここまでになります。
ここからは、App 全体に Material-UI の Themes (MuiTheme) を適用する方法を見ていきます。 Material-UI v3 では App の最上位に位置する Component に withRoot
関数を適用することで、App 全体の Components に MuiTheme のスタイルを適用します。
.── src
└── index.tsx (entry point)
└── App.tsx
└── components
│ └── FirstComponent.tsx
│ └── SecondComponent.tsx
│ └── ThirdComponent.tsx
└── utils
└── withRoot.tsx
./src/utils/withRoot.tsx
./src/utils
ディレクトリに withRoot.tsx
を作成します。
withRoot
関数は、App 内の最上位の Component に一度だけ適用します。この関数を適用した Component 傘下のすべての Components に MuiTheme を供給します。
import * as React from 'react';
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
// 任意の Theme Colors
import lightblue from '@material-ui/core/colors/lightBlue';
import blueGrey from '@material-ui/core/colors/blueGrey';
import CssBaseline from '@material-ui/core/CssBaseline';
const theme = createMuiTheme({
// Theme Colors
palette: {
primary: lightblue,
secondary: blueGrey,
},
// typography
typography: {
useNextVariants: true
}
});
function withRoot<P>(Component: React.ComponentType<P>) {
function WithRoot(props: P) {
return (
<MuiThemeProvider theme={theme}>
<CssBaseline />
<Component {...props} />
</MuiThemeProvider>
);
}
return WithRoot;
}
export default withRoot;
theme で App 全体の色設定や Material-UI Components のスタイルを定義することができます。
Themes の記述方法は公式ドキュメントを参照ください。
./src/App.tsx
import * as React from "react";
import { Theme } from "@material-ui/core/styles/createMuiTheme";
import withStyles, {
WithStyles,
StyleRules
} from "@material-ui/core/styles/withStyles";
import createStyles from "@material-ui/core/styles/createStyles";
// Components
import ThirdComponent from './components/ThirdComponent';
// withRoot を import
import withRoot from './utils/withRoot';
// styles を定義
const styles = (theme: Theme): StyleRules => createStyles({
root: {
}
});
// 型定義 Props を定義
type Props = WithStyles<typeof styles>;
// App Component を定義
const App: React.FC<Props> = ({ classes }: Props) => (
<div className={classes.root}>
<ThirdComponent title="React TypeScript Material-UI Example" />
</div>
);
// withRoot で export
export default withRoot(withStyles(styles)(App));
この例では App.tsx
が最上位に来るので、withRoot
関数をスタイル付き Component である withStyles(styles)(App)
に適用して export しています。これで App 傘下のすべての Components に MuiTheme が適用されます。
./src/index.tsx (エントリーポイント)
エントリーポイントである ./src/index.tsx
は普通の React App と同じ書き方です。
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<App />,
document.getElementById('root') as HTMLElement
);
TypeScript で Material-UI v3 を扱った情報は少なかったのでまとめてみました。
参考
- Material-UI TypeScript Guide / Material-UI
- Create React App example with TypeScript / GitHub
- React & Redux in TypeScript - Static Typing Guide / GitHub