NextとかGatsbyとかのJS製静的サイトジェネレータのフレームワークでたどり着いた個人的なスタイリングのベストプラクティス

3 min read読了の目安(約3500字

はじめに

長いタイトルですが今回は短編です。
飽くまでこんな方法がそういえばあったな、くらいでご覧ください。

最近名前聞かないけれど

aphrodite

https://github.com/Khan/aphrodite

私もNextを使い始めてからは存在をすっかり忘れていましたが、最近は専らaphroditeを使用しています。

  1. スタイリングをするだけのコンポーネントとロジックを持つコンポーネントを完全に分割できる
  2. ベースのスタイルを持ったコンポーネントを定義して、上書きする形で自由なスタイルを当てられる
  3. サーバーサイドでhead内にスタイルを先に読み込むことができるので、スタイルの読み込み遅延が発生しづらい。

この辺のパフォーマンスとか管理面のメリットについては'19年のairbnbのReact confでのプレゼンが参考になります。

https://www.youtube.com/watch?v=fHQ1WSx41CA

以下、使用例(Next.js)

_document.tsx
import Document, {
    Html,
    Head,
    Main,
    NextScript,
    DocumentContext
} from 'next/document';
import { StyleSheetServer } from 'aphrodite';

type WithStyleProps = {
    ids: string[];
    css: {
    	content: string;
    	renderedClassNames: string[];
    };
};

export default class MyDocument extends Document<WithStyleProps> {
    static async getInitialProps({ renderPage }: DocumentContext) {
	const { html, css } = StyleSheetServer.renderStatic(
            () => renderPage() as any
	) as {
            html: any;
            css: {
	        content: string;
                renderedClassNames: string[];
	    };
        };
	const ids = css.renderedClassNames;
	return { ...html, css, ids };
    }

    render(): JSX.Element {
	const { css, ids } = this.props;
	return (
	    <Html>
                <Head>
	            <style
                        data-aphrodite
			dangerouslySetInnerHTML={{
			    __html: css.content
			}}
		</Head>
		<body>
		    <Main />
		    <NextScript />
		    {ids && (
		        <script
			    dangerouslySetInnerHTML={{
			        __html: `window.__REHYDRATE_IDS = ${JSON.stringify(
				    ids
				)}`
			    }}
			/>
		    )}
		</body>
	    </Html>
	);
    }
}
_app.tsx
import { AppProps } from 'next/app';
import { StyleSheet } from 'aphrodite';

if (typeof window !== 'undefined') {
    StyleSheet.rehydrate(window.__REHYDRATE_IDS);
}

const App = ({ Component, pageProps }: AppProps): JSX.Element => {
    return <Component {...pageProps} />;
}
Container.tsx
import { StyleDeclaration, css, StyleSheet } from 'aphrodite';
import { HTMLAttributes } from 'react';

export type ContainerStyles = StyleDeclaration<{
    container: unknown;
}>;

type Props = HTMLAttributes<HTMLDivElement> & {
    styles?: ContainerStyles;
};

const containerBaseStyles = StyleSheet.create({
    container: {
        background: 'transparent';
	position: 'relative';
    }
});

const Container: forwardRef<HTMLDivElement, Props>(
    ({ styles, className, children, ...props }, ref) => (
        <div
	    ref={ref}
	    {...props}
	    className={`
	        ${css(
		    containerBaseStyles.container,
		    styles && StyleSheet.create(styles).container
		)} ${className}
	    `}
        >
	    {children}
	</div>
    )
);

export default Container;

このContainerコンポーネントはただのdivを出力するコンポーネントですが、
独自のクラス名を渡すこともできるし、aphroditeのお作法に乗っ取りstylesのpropsにオブジェクト形式でスタイルを渡すこともできます。
コレを使うことで、例えばカラーパレットやpaddingなどの値をcss変数ではなくtypescriptの変数で持たせておくこともできます。

css変数が動かないブラウザ(IEなど)での利用が想定される場合など、おすすめです。

SomeComponent.ts
import { FC } from 'react';
import Container from './Container';

const SomeComponent: FC = () => (
    <Container styles={{
        container: {
	    background: 'red',
	    ':before': {
	        content: '""',
		position: 'absolute'
	    }
	}
    }}>
        <p>Some child elements.</p>
    </Container>
);

追記

https://techlife.cookpad.com/entry/2021/03/15/090000
CSS in JSについてはcookpad様も採用されたらしい。

aphrodite自体は決して新しくないけど、CSS in JS自体は支持されてますね。
今の人気はemotionとかstyled-componentなのかな。

以上でした。