styled-components
インストール
以下のコマンドを実行するだけで良い
code:sh
$ npm install styled-components
サンプルコード
code:tsx
import styled from "styled-components";
const ScaledImg = styled.img` object-fit: cover;
width: 100%;
height: 500px;
`;
styled には各 HTML のタグに対応する関数(タグ関数)が提供されている styled-components ライブラリはクラス名を自動生成し対応するクラスに適用する
code:html
<img src="..." class="sc-blHHSb gaBufi" />
sc-*: styled-components がセレクタに使用する安定したクラス名
ランダムな文字列: スタイルのハッシュから生成されたクラス名
これらの読みづらいクラス名は、Babel を用いることで解消できる そして、これらのスタイルルールをページの <head> 内にある <style> に挿入する
code:css
<style data-styled="active" data-styled-version="6.1.13">
.gaBufi{object-fit:cover;width:100%;height:500px;}
</style>
Props をスタイルに渡す場合
各 HTML のタグに対応する関数の型パラメータに渡した型が、その関数によって返されるコンポーネントが受け取る Props になる
code:tsx
const DEFAULT_IMG_HEIGHT = "500px";
export type CarouselSlideProps = {
imgUrl?: string;
/** @default "500px" */
imgHeight?: string | number;
description?: ReactNode;
attribution?: ReactNode;
} & ComponentPropsWithRef<"figure">;
type ImgComponentProps = {
};
const ScaledImg = styled.img<ImgComponentProps>`
object-fit: cover;
width: 100%;
height: ${(props) =>
typeof props.$height === "number" ? ${props.$height}px : props.$height};
`;
const CarouselSlide = ({
imgUrl,
imgHeight = DEFAULT_IMG_HEIGHT,
description,
attribution,
...rest
}: CarouselSlideProps) => (
<figure {...rest}>
<ScaledImg src={imgUrl} $height={imgHeight} />
<figcaption data-testid="caption">
<strong>{description}</strong> {attribution}
</figcaption>
</figure>
);
ImgComponentProps のフィールドに付いている $ 接頭辞は、スタイルの補完に用いられる一時的な Props を示す styled-components の慣習
この Props は DOM 内の <img> には渡されない Props(imgHeight)が変更されると、自動的に更新される
公式が提供しており用いることを推奨
メリット
クラス名の接頭辞が sc-* ではなく、CarouselSlide__ScaledImg- のようにモジュール名とコンポーネント名が表示されるようになる
これによりデバッグが容易に
パフォーマンスが向上する
テンプレートリテラル内のコメントや不要な空白が削除されるため(Minify) warning.icon Vite の react プラグインは内部で Babel を用いている ため、簡単に導入可能
インストール
code:sh
$ npm install --save-dev babel-plugin-styled-components
vite.config.ts の修正
code:vite.config.ts
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
[
"babel-plugin-styled-components",
{
displayName: true,
fileName: true,
},
],
],
},
}),
],
// ...
});
インストール
code:sh
$ npm install --save-dev stylelint stylelint-config-standard postcss-styled-syntax
stylelint: Stylelint 本体
stylelint-config-standard: Styleling のデフォルトルールを拡張する
.stylelintrc ファイルの作成
code:.stylelintrc
{
"customSyntax": "postcss-styled-syntax",
"allowEmptyInput": true
}
.stylelintignore ファイルの作成
dist ディレクトリを無視するように設定する
code:.stylelintignore
dist
package.json の scripts にコマンドを追加する
code:package.json
{
"scripts": {
"dev": "vite",
"test": "vitest",
"lint": "npm run lint:js && npm run lint:css",
"lint:js": "eslint . && prettier --list-different .",
"lint:css": "stylelint \"**/*.{ts,tsx}\"",
"format": "npm run format:js && npm run format:css",
"format:js": "eslint --fix . && prettier --log-level warn --write .",
"format:css": "stylelint \"**/*.{ts,tsx}\" --fix",
"build": "tsc -b && vite build",
"preview": "vite preview"
}
}
スタイルでアサーションする
jest-styled-components プラグインを用いる必要がある
インストール
code:sh
$ npm install --save-dev jest-styled-components
vite.config.ts の defineConfig に渡している test.setupFiles のファイルでプラグインを読み込む
code:vite.config.ts
export default defineConfig({
test: {
globals: true,
environment: "happy-dom",
},
});
code:test-setup.ts
import "@testing-library/jest-dom/vitest";
import "jest-styled-components";
これでいちいち import する必要がなくなる
スタイルをアサーションする関数
テスト対象コンポーネント
code:tsx
const DEFAULT_IMG_HEIGHT = "500px";
export type CarouselSlideProps = {
imgUrl?: string;
/** @default "500px" */
imgHeight?: string | number;
description?: ReactNode;
attribution?: ReactNode;
} & ComponentPropsWithRef<"figure">;
type ImgComponentProps = {
};
const ScaledImg = styled.img<ImgComponentProps>`
object-fit: cover;
width: 100%;
height: ${(props) =>
typeof props.$height === "number" ? ${props.$height}px : props.$height};
`;
const CarouselSlide = ({
imgUrl,
imgHeight = DEFAULT_IMG_HEIGHT,
description,
attribution,
...rest
}: CarouselSlideProps) => (
<figure {...rest}>
<ScaledImg src={imgUrl} $height={imgHeight} />
<figcaption data-testid="caption">
<strong>{description}</strong> {attribution}
</figcaption>
</figure>
);
テスト
code:tsx
it("has the expected static styles", () => {
render(<CarouselSlide />);
const img = screen.getByRole("img");
expect(img).toHaveStyleRule("object-fit", "cover");
expect(img).toHaveStyleRule("width", "100%");
});
it("uses imgHeight as the height of the <img>", () => {
render(<CarouselSlide imgHeight="123px" />);
expect(screen.getByRole("img")).toHaveStyleRule("height", "123px");
});
スタイルの拡張
既存の styled-components で生成したコンポーネントを styled に渡すと、元のコンポーネントのスタイルに新しいスタイルを追加した新しいコンポーネントを返す
e.g.
コンポーネント
code:tsx
const DEFAULT_IMG_HEIGHT = "500px";
export type CarouselSlideProps = {
ImgComponent?: (
props: ComponentPropsWithRef<"img"> & ImgComponentProps,
) => JSX.Element;
imgUrl?: string;
/** @default "500px" */
imgHeight?: string | number;
description?: ReactNode;
attribution?: ReactNode;
} & ComponentPropsWithRef<"figure">;
type ImgComponentProps = {
};
export const ScaledImg = styled.img<ImgComponentProps>`
object-fit: cover;
width: 100%;
height: ${(props) =>
typeof props.$height === "number" ? ${props.$height}px : props.$height};
`;
const CarouselSlide = ({
ImgComponent = ScaledImg,
imgUrl,
imgHeight = DEFAULT_IMG_HEIGHT,
description,
attribution,
...rest
}: CarouselSlideProps) => (
<figure {...rest}>
<ImgComponent src={imgUrl} $height={imgHeight} />
<figcaption data-testid="caption">
<strong>{description}</strong> {attribution}
</figcaption>
</figure>
);
テスト
ImgComponent には、styled で ScaledImg を拡張したコンポーネントを渡すことが可能
code:tsx
it("allows styles to be overridden with ImgComponent", () => {
const TestImg = styled(ScaledImg)`
width: auto;
object-fit: fill;
`;
render(<CarouselSlide ImgComponent={TestImg} imgHeight={250} />);
expect(screen.getByRole("img")).toHaveStyleRule("width", "auto");
expect(screen.getByRole("img")).toHaveStyleRule("height", "250px");
expect(screen.getByRole("img")).toHaveStyleRule("object-fit", "fill");
});