💈

【Next.js】styled-componentsにおけるprops,mixin, スタイルの継承についてふれる

2024/09/11に公開

React/Next.jsでの開発において、コンポーネントへのスタイル適用は主にstyled-componentsを使っています。しかし、改めてその機能を学び直してみると、mixinやスタイルの継承といった使い方ができることを知り、自分がstyled-componentsのごく表層の基本機能しか活用できていなかったことに気づかされました。
本記事では、styled-componentsを使ったpropsやmixin、スタイルの継承の実装方法についてまとめたいと思います。

実行環境

本記事で使用している環境は以下の通りです。

  • Next.js: 14.2.5
  • React: 18.0.0
  • styled-components: 6.1.12
  • TypeScript: 5.0.0

準備:プロジェクトへのstyled-componentsの導入

パッケージのインストール

npm install --save styled-components
npm install --save-dev @types/styled-components

next.config.jsに設定を追加

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  compiler: {
    styledComponents: true,
  }
};

export default nextConfig;

これでTypeScript環境のNext.jsにおいてstyled-componentsを使用できるようになりました。
ですが、この段階ではまだ、SSRやSSG使用時にサーバーサイドで生成される静的ファイルにはスタイルは適用できない状態です。
そのため、以下の設定を行い、サーバーサイドでもスタイルが適用されるようにしていきます。

pages/_document.tsxを記述する

pages/_document.tsxは、Next.jsで「カスタムドキュメント」と呼ばれる仕組みで、HTML構造(html、head、bodyタグなど)に関わる部分を上書きするためのファイルです。デフォルトの設定を拡張して、サーバーサイドレンダリング時にstyled-componentsのスタイルを適用するために、このファイルをカスタマイズします。
Routing: Custom Document

import Document, { DocumentContext } from 'next/document'
import { ServerStyleSheet } from 'styled-components'

export default class MyDocument extends Document {     // デフォルトのDocumentを拡張して、独自のスタイルを適用するためのカスタマイズ
  static async getInitialProps(ctx: DocumentContext) {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        })
     
      // デフォルトの設定を取得する     
      const initialProps = await Document.getInitialProps(ctx)
      
      return {
        ...initialProps,
        styles: [
          initialProps.styles,    // デフォルトのスタイルに
          sheet.getStyleElement()  // styled-componentsのスタイルを追加する
        ],
      }
    } finally {
      sheet.seal()
    }
  }
}

基本的な構造はほぼ決まっているため、「おまじない」のように感じるかもしれませんが、このファイルでは、既存のNext.jsのDocumentに対してstyled-componentsのスタイルをサーバーサイドで適用できるように設定しています。

styled-componentsの基本的な使い方

まずはコンポーネントにスタイルを当てるためのstyled-componentsの基本的な使い方について見ていきます。

スタイルを当てたコンポーネントは以下のような形式で定義します。

const <コンポーネント名> = styled.<HTML要素名>`スタイル`

そして、ここで定義したコンポーネントをJSX構文の中で呼び出せばスタイルが反映されたコンポーネントを表示させることができます。

以下はbutton要素にスタイルを適用したPrimaryButtonコンポーネントの実装例です。

import { NextPage} from 'next'
import styled from 'styled-components'

const ButtonPrimary = styled.button`
	color: #ffffff;
	background: #0d6efd;
	border: 2px solid #0d6efd;
	font-size: 2em;
	margin: 1em;
	padding: 0.25em 1em;
	border-radius: 30px;
	cursor: pointer;
`

const StyleSample: NextPage = () => {
    return (
        <>
            <ButtonPrimary>ボタンです!</ButtonPrimary>
        </>
    )
}

export default StyleSample

propsを使ったstyled-components

これまでは、文字色や背景色など一部のスタイルだけが異なるコンポーネントを定義する際、スタイルの異なる部分だけを書き換えながら、複数のstyled-componentsをコピペして利用していました。冗長だと感じていたのですが、他に方法がわからなかったため、このアプローチを使っていました。。。

しかし、styled-componentsにはpropsを渡す方法があるということを知りました。
この手法を使えば、基本的なスタイルを共有しながら、必要な部分だけpropsを通じて変更できるようになり、コードの重複を減らすことができます。

styled-componentsでpropsを利用する方法は以下の通りです。

  • styled-componentsに渡されるpropsをあらかじめ型定義しておく。
  • styled-componentsを定義する際、タグ名の直後に定義ておいたpropsの型を指定する。
  • スタイル定義の中で${(props) => props.color}のように、propsから値を取り出す関数を埋め込む。

以下は、ボタンの文字色と背景色を呼び出し時にpropsで指定できるコンポーネントの例です。

import { NextPage} from 'next'
import styled from 'styled-components'

type ButtonProps = {
	color: string
	backgroundColor: string
}

const Button = styled.button<ButtonProps>`
	color: ${(props) => props.color};
	background: ${(props) => props.backgroundColor};
	border: 2px solid ${(props) => props.color};
	
	font-size: 2em;
	margin: 1em;
	padding: 0.25em 1em;
	border-radius: 8px;
	cursor: pointer;
`

const StyleSample: NextPage = () => {
    return (
        <>
            <Button backgroundColor="transparent" color="#FF0000">
                透明なボタン
            </Button>
            <Button backgroundColor="#0d6efd" color="white">
                青いボタン
            </Button>
        </>
    )
}

export default StyleSample

どちらのボタンもともにButtonコンポーネントを利用していますが、コンポーネントの呼び出し時にcolorbackgroundColorに対してそれぞれ異なる値を指定することで、スタイルの異なるボタンを描画することができました。

mixinを使ったstyled-components

styled-componentsではmixinを利用して、個別に定義したスタイルの設定を再利用することもできます。

styled-componentsでmixinを使う方法は以下の通りです。

  • css関数をstyled-componentsからインポートする。
  • css関数を使って再利用したいスタイル定義する。
  • 定義したスタイルをstyled-componentsの定義文の中に挿入する。

以下、利用例です。

import { NextPage} from 'next'
import styled, { css } from 'styled-components'

const outlineRed = css`
	padding: 0.25em 1em;
	border: 3px solid #ff0000;
	border-radius: 10px;
`

const fontRed = css`
	color: #ff0000;
	font-size: 2em;
`

const Button = styled.button`
  background: transparent;
	margin: 1em;
	cursor: pointer;

	${outlineRed}
	${fontRed}
`

const Text = styled.p`
	font-weight: bold;

	${fontRed}
`

const MixinStyleSample: NextPage = () => {
    return (
        <div>
            <Button>ボタンです</Button>
            <Text>テキストです</Text>
        </div>
    )
}

export default MixinStyleSample

このように、css関数を使って定義したスタイルを再利用しながらそれぞれのスタイルを定義することができました。

スタイルを継承する

またstyled-componentsでは既存のコンポーネントのスタイルを引き継ぐこともできるそうです。

例えば、スタイルが適用された既存のButtonコンポーネントがあり、そのスタイルを一部変更した別のButtonコンポーネントを作りたい場合を考えてみます。このケースでは、先ほど紹介したpropsやmixinの機能を活用して、コードの重複を避ける方法も選択肢の一つです。
しかし、この場合、既存のstyled-componentsをpropsを受け取れるように修正する必要があり、また、そのコンポーネントが既に複数の箇所で使用されている場合、各コンポーネントに渡されるpropsのメンテナンスも必要となり非常に煩雑な作業となります。
さらに、css関数を使用する場合も、再利用したいスタイルを別途css関数で定義する必要があり、これも場合によっては手間のかかる作業になります。

そんな時に便利なのが、styled-componentsのスタイルの継承機能です。styled関数に既に定義されているstyled-componentsの要素名を引数として渡すことで、そのコンポーネントのスタイルをそのまま継承することができます。

以下は既に定義しているTextコンポーネントのスタイルを継承しつつ、新しいスタイルを加えたBorderedTextコンポーネントの例です。

import { NextPage} from 'next'
import styled from 'styled-components'

const Text = styled.p`
	font-weight: bold;
	color: blue;
`

const BorderedText = styled(Text)`
	padding: 8px 16px;
	border: 3px solid blue;
	border-radius: 10px;
`

const InheritStyleSample: NextPage = () => {
    return (
        <div>
            <Text>私はただのテキストです</Text>
            <BorderedText>枠線が付きました</BorderedText>
        </div>
    )
}

export default InheritStyleSample

このようにBorderedTextコンポーネントではテキストの色や太さは定義していませんが、Textコンポーネントを継承したことで、そのスタイルを再利用することができています。

既存のスタイルを別のHTML要素に対して使用する

stlyed-componentsは定義時にHTML要素を指定しますが、例えばp要素に対して適用したスタイルをa要素にも同様に適用したい場合を考えてみます。この場合、スタイル適用先のHTML要素が異なるので、それぞれ別々にstyled-componentsを定義する必要があると思っていました。

しかし、styled-componentsでは一度定義したコンポーネントのスタイルを別のHTML要素に転用させることができるそうです。

方法としては定義したstyled-compoentnsを呼び出す際に、asを用いて転用したいHTML要素を指定します。
以下はp要素として定義したスタイルをa要素やbutton要素に転用する例です。

import { NextPage} from 'next'
import styled from 'styled-components'

const Text = styled.p`
	font-size: 2em;
	color: #1e90ff;
`

const ConvertStyleSample: NextPage = () => {
	return (
        <div>
            <Text>私はただのテキストです</Text>
            <Text as="a" href='/'>リンク要素になれるかもしれません</Text>
            <br />
            <Text as="button">ボタン要素になることもできそうです</Text>
        </div>
    )
}

export default ConvertStyleSample


p要素として定義したTextコンポーネントがa要素になったり、button要素になったりしていますね。
この機能を使用する場面は限定されそうな気がしますが、大変便利であるということがわかります。

以上、styled-componentsnの基本的な使い方からpropsやmixin,スタイルの継承などの機能についてふれてみた記録です。
ここまでお読みいただきありがとうございました。

Discussion