Reactのカラーテーマはこうしてこう[Material-UI, styled-componentsの例]

# TL;DR

この記事は、[メタップス Advent Calendar 2020](https://qiita.com/advent-calendar/2020/metaps)7日目の記事です。
本当にやりたかったテーマ(Reactで新規プロジェクト立ち上げるときはこうしてこう)に間に合わなかったため、やや細かい内容になってしまいました。お許しください。

ここでは、ReactでThemingするとき、カラーテーマをどんな感じでやると良いか、Material-UIやstyled-componentsのThemeProviderを例にとって、In My Opinionで解説します！


# 方針

原則として、UIライブラリ(Material-UI)のthemeを大きく変えずに拡張する。
それをCSS in JS(styled-components)のテーマにも適用することで、型システムを利用できるようにする。

* primaryｙｓsecondaryは、プロジェクトで決めているテーマカラーを素直に入れていく
* それで足りない色は型を注入してユーザー定義していく
* Material-UIとstyled-componentsのThemePrivider両方で共通のthemeオブジェクトを利用する
* 当然typescriptでコードヒントを効かす

# 解説と実装例

## Material-UIのThemeProvider

Material-UIのColor paletteは、primary, secondary, error, warning, ...と存在し、以下のような構造のインタフェースになっています。

```ts
export interface SimplePaletteColorOptions {
  light?: string;
  main: string;
  dark?: string;
  contrastText?: string;
}

...

export interface Color {
  50: string;
  100: string;
  200: string;
  300: string;
  400: string;
  500: string;
  600: string;
  700: string;
  800: string;
  900: string;
  A100: string;
  A200: string;
  A400: string;
  A700: string;
}

...

export type ColorPartial = Partial<Color>;

...

export type PaletteColorOptions = SimplePaletteColorOptions | ColorPartial;

...

export interface PaletteOptions {
  primary?: PaletteColorOptions;
  secondary?: PaletteColorOptions;
  ...
}
```

かなり細かく設定できますが、primary、secondaryを除けば、infoやsuccessなどの汎用的なカラーリングであり、プロジェクト規模がある程度まで行くと、これでは足りなくなってしまいます。

例えば、Material-UIで定義されているパレットに加えて、red, pink, greenを、定義済みのパレットと同じようなインタフェースで新設したいときは、どうすればよいのでしょうか。

たまに見かけるやり方で、、theme.tsxなどに、Material-UIの`createMuiTheme`とは別にカラーのオブジェクトを定義してエクスポートし、利用する場所でimportするというのがあります。

```theme.tsx
export const theme = createMuiTheme({
...
});

export const userPalette = {
    red: {
        ...
    }
}

```

しかし、毎回importするのは面倒ですし、これではコードヒントも効きません。
themeはネストが深くなるので、きちんと型をつけてコードヒントの恩恵を受けたいです。

なのでここは型を注入します。

```types/createPalette.d.ts
import * as createPalette from "@material-ui/core/styles/createPalette";
import { PaletteColorOptions } from "@material-ui/core/styles/createPalette";

interface CustomPalette {
  red: PaletteColorOptions; // light, main, dark, 50, 100, ..., 900, A100, ..., A700
  pink: PaletteColorOptions;
  green: PaletteColorOptions;
}

declare module "@material-ui/core/styles/createPalette" {
  // eslint-disable-next-line
  interface PaletteOptions extends CustomPalette {}
  // eslint-disable-next-line
  interface Palette extends CustomPalette {}
}
```

これで型が拡張されました。
`ThemePrivider.tsx` で、拡張した型を追加でthemingしてみます。

またここでも、Material-UIのカラーパレットシステムを利用します。(自分で一つずつ設定しても良いですが)
Material-UIでは、ある程度一般的な色のパレットをimportできるように準備されています。
https://material-ui.com/customization/color/#color-palette

これを利用すると、以下のように非常にシンプルに記述することができます。

```ThemePrivider.tsx
import { red, pink, green } from "@material-ui/core/colors";

export const theme = createMuiTheme({
  palette: {
    red: {
      ...red,
      light: "#e57373",
      main: "#f44336",
      dark: "#d32f2f",
    },
    pink: {
      // redと同様
    },
    green: {
      // redと同様
    },
  },
});

```

これで新たに、17色 * 3の色がテーマから使えるようになっています。

![Screen Shot 2020-12-07 at 6.30.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174325/b6b0d754-d039-32b0-761c-e5e861962305.png)

あとは、受け取ったchildrenをラップするようにコンポーネントを定義し、ルートコンポーネントでラップするだけです。

```ThemeProvider.tsx
import { ThemeProvider as MuiThemeProvider } from "@material-ui/styles";

export const ThemeProvider: React.FC<Props> = ({ children }: Props) => {
  return (
    <MuiThemeProvider theme={createMuiTheme(theme)}>
      {children}
    </MuiThemeProvider>
  );
};
```

## styled-componentsのThemeProvider

以上でMaterial-UIでのThemingはできました。
次はstyled-componentsのThemeProviderにも、同じthemeオブジェクトを渡して、styled内でpropsとして利用できるようにしたいです。

しかし、そのままThemeProviderにthemeを渡すと、tscでエラーが出るので、ここでも型を注入します。

Material-UIのThemeインタフェースを利用してシンプルに書くことができます。

```types/styled-components.d.ts
import "styled-components";
import { Theme } from "@material-ui/core";

declare module "styled-components" {
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
  export interface DefaultTheme extends Theme {}
}
```

こうしておいて、ThemeProviderでさらにラップすれば良いです。

```ThemeProvider.tsx
import { ThemeProvider as MuiThemeProvider } from "@material-ui/styles";
import { ThemeProvider as StyledThemeProvider } from "styled-components";

export const ThemeProvider: React.FC<Props> = ({ children }: Props) => {
  return (
    <MuiThemeProvider theme={createMuiTheme(theme)}>
      <StyledThemeProvider theme={theme}>{children}</StyledThemeProvider>
    </MuiThemeProvider>
  );
};
```

あとは普通に使うだけ。
styled-componentsのThemeProviderは、下位にある全てのコンポーネントツリーに対してこのthemeを送るため、全てのstyledでthemeにアクセスできるようになり、importする必要はなくなります。
コードヒントも有効になります。

```tsx
const StyledRedWrapper = styled.div`
  background-color: ${(props) => props.theme.palette.red[50]};
`;
```

# おわり

これで、Material-UIのテーマを拡張して適用し、また、styled-compoenentsでも同じものを利用できるようになりました。
今回はMaterial-UI & styled-componentsでしたが、Material-UIがAntDesignでも、styled-componentsがemotionでも、あるいはどちらか片方しか使わなくても、やることは基本的に同じだと思います。

[方針](https://qiita.com/tachibanayu24/items/b93537fced5c17f3d42d#%E6%96%B9%E9%87%9D)に書きましたが、

> 原則として、UIライブラリ(Material-UI)のthemeを大きく変えずに拡張する。
> それをCSS in JS(styled-components)のテーマにも適用することで、型システムを利用できるようにする。

で良いのかなと思います。