水戸地図(β)

2019年01月12日

TypeScript + React + Material-UI v3 のスタイル付き Components ガイド

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://cieloazul310.github.io/2019/06/typescript-material-ui-v4/

更新

  • 2019/02/23: React.FunctionComponentReact.FC に書き換えました。
  • 2019/02/23: (props: Props) => {}({ classes }: Props) => {} に書き換えました。
  • 2019/06/04: Material-UI v4 に関する追記を行いました。

環境

create-react-app で作成したディレクトリを前提にしています。
tsconfig.jsontslint.json の設定はここでは省きます。

参考: TypeScript React Starter

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 の作成を、

  1. Function Components (props あり)
  2. Function Components (props なし)
  3. 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 から WithStylesStyleRulesという型定義を、
@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. 型定義 PropsWithStyles<typeof styles> で拡張する

props の型定義 PropsWithStyles<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 に定義した classesKey をいちいち書く必要がなくなります。

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 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 Girl / Madonna (1984)

広告

2019年01月12日 最終更新日2019年06月04日

TypeScript + React + Material-UI v3 のスタイル付き Components ガイド

技術記事

Top

水戸地図(β)