🛠️

Next.jsにtwin.macro導入(Next.js, Tailwind, styled-components, TypeScript)

2022/09/03に公開

はじめに(追記)

tailwindについては導入のメリットについて考え直してみました(以下に2022/9時点での考えをまとめてみました)。
https://zenn.dev/yutaro_matsu/scraps/8f8dddc5c47eb9

個人的には、CSS周りのライブラリは基本的にstyled-componentsだけ(もしくはその他のCSS-in-JSライブラリ)でも良いのでは?というのが現状の所感です。
ですが、今の所は「デザインシステムを簡単に導入できること」には少なからず価値を感じてますので、私は個人開発でtailwindを導入しております。
また、Material-UIをはじめとるすUIライブラリはあまり好きではありません(UIが限定的になったり、FWや他ライブラリとの併用時に考慮することが増えるため)。
上記の理由から、今の所はtailwind + CSS-in-JSがしっくりきています。

この記事でやること、やらないこと

この記事でやらないこと

  • next.jsの環境構築
  • tailwindの導入

上記については下記公式ドキュメント等をご参考にしてください。
https://v2.tailwindcss.com/docs/guides/nextjs

この記事でやること

基本的に以下の公式ドキュメント(Git)に書いてある通りのことをやります。
この記事で「公式ドキュメント」と表現しているものは、全て以下ページのものを指しています。
https://github.com/ben-rogerson/twin.examples/tree/master/next-styled-components-typescript

twin.macroとは?

公式のドキュメントには、以下のキャッチフレーズ?があります。

Twin blends the magic of Tailwind with the flexibility of css-in-js

要はTailwindでcss-in-jsを利用するためのライブラリだと認識しています。
基本的な機能の一つとしては、tailwind単体だとclassName内にstyleをあてますが、twin.macroを導入すると、twという属性にstyleをあてることになります。

https://github.com/ben-rogerson/twin.examples/tree/master/next-styled-components-typescript

実際にtwin.macroを導入する

必要なライブラリのインストール

yarn add styled-components
yarn add -D twin.macro babel-plugin-macros babel-plugin-styled-components @types/styled-components 

.babelrc.jsを作成

babelについてはまだあまり詳しくないので、細かい話は割愛させてください。

.babelrc.js
/**
 NOTE:
   Babelとは
    jsのコンパイラ。
    前提としてjsには世代(ES2015, ES2016など)ごとの言語仕様がある。
    Babelはブラウザごとの「上記世代へのサポートの違い」を解消してくれる、Node.js製のツール。
*/
module.exports = {
  presets: [['next/babel', { 'preset-react': { runtime: 'automatic' } }]],
  plugins: [
    'babel-plugin-macros',
    ['babel-plugin-styled-components', { ssr: true }],
  ],
}

_document.tsxを作成する

これも公式ドキュメントにあるやつです。
このファイルを用意しないと、スタイルが適用されていない画面が一瞬ちらつきます(実際に違いを試してみました)。
公式ドキュメントには、このファイルについて以下のような説明が書かれています。

To avoid the ugly Flash Of Unstyled Content (FOUC), add a server stylesheet in pages/_document.tsx that gets read by Next.js:

_document.tsx
import React from 'react'
import Document, { DocumentContext } from 'next/document'
import { ServerStyleSheet } from 'styled-components'

export default class MyDocument extends 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: [
          <React.Fragment key="styles">
            {initialProps.styles}
            {sheet.getStyleElement()}
          </React.Fragment>,
        ],
      }
    } finally {
      sheet.seal()
    }
  }
}

twin.d.tsを作成する

なぜか公式ドキュメントのREADMEには書かれていないのですが、この設定は非常に重要です。
最初に紹介した公式ドキュメントのページにある、typesフォルダの中にもしっかり用意されています。
ファイルのリンクも共有します(こちら)。
以下は、公式ドキュメントの特に重要だと感じた部分のみを抜粋してます。

twin.d.ts
import 'twin.macro'
import styledImport, { CSSProp, css as cssImport } from 'styled-components'

// ①
declare module 'twin.macro' {
  // The styled and css imports
  const styled: typeof styledImport
  const css: typeof cssImport
}

// ②
declare module 'react' {
  // The css prop
  interface HTMLAttributes<T> extends DOMAttributes<T> {
    css?: CSSProp
    tw?: string
  }
}

以下にこちらについて補足説明します。

  • このファイル全体について
    .d.tsファイルというのは、型定義ファイルと呼ばれるみたいです(参考)。
    declareで宣言している部分は、アンビエント宣言という機能で、ライブラリに型情報を付加する?ための機能とのことです(参考)。

  • ①の部分について
    この部分は、以下の例のようなimportを可能にするための記述です。
    styled, cssは実際はそれぞれstyled-componentsの機能ですが、アンビエント宣言をしたことでtwin.macroからインポートできるようになります。
    ちなみに、スタイルを当てる場合はtwin.macroは各ファイルでimportをする必要があるので、この設定をしておくと便利だと思います。

test.tsx
// 例
import { styled, css } from 'twin.macro'
  • ②の部分について
    この部分は、html属性の型情報を付加しています。
    この設定により、各種htmlタグでtw属性、css属性を設定可能になります。
    公式ドキュメントにはtwも宣言されてますが、実はtwはここで記述しなくても問題ないです(twin.macroのデフォルト機能でhtml属性にtwが利用できる認識です)。
    この設定がどのように役立つかは、こちらの公式ドキュメントを確認するとわかりやすいかと思います。

実装例

あくまで一例ではありますが、私が試しに実装してみたソースコードを載せてみます。
公式ドキュメントのこちらや、こちらを参考にして、できるだけjsxを汚さないように実装してみました。
もしこうした方が良いなどありましたら、助言いただけますととてもありがたいです!

baseStyles.ts
import tw from 'twin.macro'

export type ButtonType = 'green' | 'black'

export type ButtonProps = {
  className?: string
  type: ButtonType
  title: string
  onClick: () => void
}

export type CustomButtonProps = { isHover: boolean }

export const BaseButtonStyle = tw`flex items-center justify-center h-9 px-5 rounded-lg border-2 font-bold`

const GreenButton = {
  button: ({ isHover }: CustomButtonProps) => [
    tw`bg-white border-green40`,
    isHover && tw`transition bg-green40`,
  ],
  title: ({ isHover }: CustomButtonProps) => [
    tw`text-green40`,
    isHover && tw`transition text-white`,
  ],
}

const BlackButton = {
  button: ({ isHover }: CustomButtonProps) => [
    tw`border-black`,
    isHover && tw`transition bg-black`,
  ],
  title: ({ isHover }: CustomButtonProps) => [
    tw`font-black`,
    isHover && tw`transition text-white`,
  ],
}

export const ButtonStyle = {
  green: GreenButton,
  black: BlackButton,
}

Button.tsx
import 'twin.macro'
import { useState } from 'react'
import { BaseButtonStyle, ButtonProps, ButtonStyle } from './baseStyles'

export const Button = ({ className, type, title, onClick }: ButtonProps) => {
  const [isHover, setIsHover] = useState<boolean>(false)

  return (
    <button
      css={[BaseButtonStyle, ButtonStyle[type].button({ isHover })]}
      className={className}
      onClick={onClick}
      onMouseOver={() => setIsHover(true)}
      onMouseLeave={() => setIsHover(false)}
    >
      <span css={[ButtonStyle[type].title({ isHover })]}>{title}</span>
    </button>
  )
}

最後に

どのようにComponentを実装していくかも上記と同じこちらの公式ドキュメントを確認するとわかりやすいかと思います。

以上です!

Discussion