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

<!-- Lead Paragraph -->
React + [Material-UI v3][Material-UI] では `withStyles(styles)(Component)` という独自の記法で**スタイル付き Components** を生成します。
TypeScript で Material-UI のスタイル付き Components を記述する場合は、従来の JavaScript での記法とは多少異なるのですが、 TypeScript を使った記法については情報が少ないと思ったので覚え書きとして残しておきます。

## Material-UI v4 について

2019年5月末に Material-UI v4 がリリースされました。記法が変わったので、v4 の記法は[別記事](https://qiita.com/cieloazul310/items/3088207acaa15a94bd44)にしています。
この先は 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 Starter](https://github.com/Microsoft/TypeScript-React-Starter#typescript-react-starter)

### TypeScript React App の作成

```Shell
$ create-react-app my-ts-app --scripts-version=react-scripts-ts
$ cd my-ts-app
$ yarn add @material-ui/core @material-ui/icons
```

### バージョン

```txt
- @material-ui/core @3.8.3
- react @16.7.0
- react-dom @16.7.0
- react-scripts-ts @3.1.0
```

### ディレクトリ構成

```txt
.── 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 を作成する方法を見ておきましょう。

```jsx
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 ガイド](https://material-ui.com/guides/typescript/#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] を参考にしています。

### 共通するコード

ここから先のコードで共通するのは以下の型定義と関数のインポートです。

```TypeScript
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 あり)

```tsx:./src/components/FirstComponent.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';

// 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 を使う場合は、

```tsx
const styles = (theme: Theme): StyleRules => createStyles({
  root: {
    fontSize: 18
  },
  header: {
    backgroundColor: theme.palette.primary.main
  }
});
```

theme を使わない場合は、

```TypeScript
const styles: StyleRules = createStyles({
  root: {
    fontSize: 18
  }
});
```

という書き方をします。
`createStyles` 関数を使うことで型拡大を防ぐことができるようです。

参考: [Using createStyles to defeat type widening](https://material-ui.com/guides/typescript/#using-createstyles-to-defeat-type-widening)

##### 2. styles の関数の引数 theme に型定義 `Theme` を与える

引数 theme に型定義 `Theme` を与えることで、エディタの補助機能が効くようになり、theme を使ったスタイリングが楽になります。
theme について詳しくは[公式ドキュメント](https://material-ui.com/customization/themes/)をお読みください。

##### 3. 型定義 `Props` を `WithStyles<typeof styles>` で拡張する

props の型定義 `Props` を `WithStyles<typeof styles>` で拡張します。
`interface Props extends WithStyles<typeof styles>` は以下の型定義 `Props` と同等のものになります。

```tsx
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 なし)

```tsx:./src/components/FirstComponent.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';

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>` を置かなければなりません。

この記法が煩雑に感じるなら、以下のような方法もあります。

```tsx
interface Props extends WithStyles<typeof styles> {
  // empty
}
```

あるいは、

```tsx
type Props = WithStyles<typeof styles>;
```

と書いた上で、 `React.FC<Props>` としても良いです。

### 3. React Component Class

class を使って State を持たない Stateless Components を作る場合

```tsx:./src/components/SecondComponent.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';

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 の場合も特に変わりません。

```tsx:./src/components/ThirdComponent.tsx
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](https://github.com/piotrwitek/react-redux-typescript-guide#stateful-components---class) に則っています。

## App に MuiTheme を適用する

ここまでは Material-UI + TypeScript におけるスタイル付き Components の作成方法を見てきました。 TypeScript 特有の記法の説明はここまでになります。

ここからは、App 全体に Material-UI の Themes ([MuiTheme](https://material-ui.com/customization/themes/)) を適用する方法を見ていきます。 Material-UI v3 では App の最上位に位置する Component に `withRoot` 関数を適用することで、App 全体の Components に MuiTheme のスタイルを適用します。

```txt
.── 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 を供給します。

```tsx:./src/utils/withRoot.tsx
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 の記述方法は[公式ドキュメント](https://material-ui.com/customization/themes/)を参照ください。

#### ./src/App.tsx

```tsx:./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 と同じ書き方です。

```tsx:./src/index.tsx
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](https://material-ui.com/guides/typescript/#typescript) / Material-UI
- [Create React App example with TypeScript](https://github.com/mui-org/material-ui/tree/master/examples/create-react-app-with-typescript) / GitHub
- [React & Redux in TypeScript - Static Typing Guide] / GitHub

<!-- References -->
[React & Redux in TypeScript - Static Typing Guide]: https://github.com/piotrwitek/react-redux-typescript-guide

[React]: https://reactjs.org/
[create-react-app]: https://github.com/facebook/create-react-app

[TypeScript]: https://www.typescriptlang.org
[Material-UI]: https://material-ui.com
[Flutter]: https://flutter.io
[Dart]: https://www.dartlang.org
[Visual Studio Code]: https://code.visualstudio.com/

[地理院地図]: https://maps.gsi.go.jp/
[Jリーグ]: https://www.jleague.jp/
[Bリーグ]: https://www.bleague.jp/