TL;DR
この記事は、メタップス Advent Calendar 20207日目の記事です。
本当にやりたかったテーマ(Reactで新規プロジェクト立ち上げるときはこうしてこう)に間に合わなかったため、やや細かい内容になってしまいました。お許しください。
ここでは、ReactでThemingするとき、カラーテーマをどんな感じでやると良いか、Material-UIやstyled-componentsのThemeProviderを例にとって、In My Opinionで解説します!
方針
原則として、UIライブラリ(Material-UI)のthemeを大きく変えずに拡張する。
それをCSS in JS(styled-components)のテーマにも適用することで、型システムを利用できるようにする。
- primaryyssecondaryは、プロジェクトで決めているテーマカラーを素直に入れていく
- それで足りない色は型を注入してユーザー定義していく
- Material-UIとstyled-componentsのThemePrivider両方で共通のthemeオブジェクトを利用する
- 当然typescriptでコードヒントを効かす
解説と実装例
Material-UIのThemeProvider
Material-UIのColor paletteは、primary, secondary, error, warning, ...と存在し、以下のような構造のインタフェースになっています。
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するというのがあります。
export const theme = createMuiTheme({
...
});
export const userPalette = {
red: {
...
}
}
しかし、毎回importするのは面倒ですし、これではコードヒントも効きません。
themeはネストが深くなるので、きちんと型をつけてコードヒントの恩恵を受けたいです。
なのでここは型を注入します。
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
これを利用すると、以下のように非常にシンプルに記述することができます。
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の色がテーマから使えるようになっています。
あとは、受け取ったchildrenをラップするようにコンポーネントを定義し、ルートコンポーネントでラップするだけです。
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インタフェースを利用してシンプルに書くことができます。
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でさらにラップすれば良いです。
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する必要はなくなります。
コードヒントも有効になります。
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でも、あるいはどちらか片方しか使わなくても、やることは基本的に同じだと思います。
方針に書きましたが、
原則として、UIライブラリ(Material-UI)のthemeを大きく変えずに拡張する。
それをCSS in JS(styled-components)のテーマにも適用することで、型システムを利用できるようにする。
で良いのかなと思います。