🧑‍🎤

【React / Linaria / CSS Modules】classNameに複数クラスを渡す時に使えるcustom hook

2022/10/28に公開

はじめに

  • CSS ModulesとLinariaをスタイリングに使用しているReactプロジェクトがあるとします。Linariaは主にcss tagを使用してスタイルを定義しています。
  • classnamesは使いません。

というなかなか限定的な前提の記事です。

*

classNameの値に複数のクラスを指定する場合、半角スペースで連結した文字列を渡します。

import styles from './index.module.scss'

<div className={`${styles['classname-a']} ${styles['classname-b']}`}>
  <p>Anya likes peanuts.</p>
</div>

join()を使うのも一般的かもしれません。

import styles from './index.module.scss'

<div
  className={[
    styles['classname-a'],
    styles['classname-b']
  ].join(' ')}
>
  <p>Pochita sacrificed his body to become Denji's heart to save him.</p>
</div>

こういうのを毎回書くのが面倒なのでcustom hookにまとめてみました。

やりたいこと

こういうことがしたいです。

{/* styleJoin([CSS Modulesのクラス名配列], [Linariaのcssで定義した変数の配列]) */}
<div
  className={
    styleJoin(['classname-a', 'classname-b'], [linariaStyle, linariaStyle2])
  }
></div>

実装

型です。

// css moduleの型
export type Styles = {
  readonly [key: string]: string
}

この型の変数をcustom hookの引数として受け付けることにします。

関数の実装

custom hookを作成して関数を実装します。
汎用的に使うstyleJoin()、Linariaのcss tagで定義したものに対してのみ使うstyleJoinLinaria()の二つを実装します。

import { Styles } from '../types/style'

type UseStyleJoinReturnType = {
  readonly styleJoin: (
    classNamesInModule?: string[],
    additionalClassNames?: string[]
  ) => string
  readonly styleJoinLinaria: (linariaCss: string[]) => string
}

/**
 * classNameに渡すstyleを結合したい時に使用する
 *
 * @param {Styles} [cssModuleStyles]
 * @return {UseStyleJoinReturnType}
 */
export const useStyleJoin = (
  cssModuleStyles?: Styles
): UseStyleJoinReturnType => {
  /**
   * 複数のクラス名をclassNameに渡す時に使用
   *
   * ex)
   * import styles from './random.module.scss'
   * <div className={styleJoin(styles, ['class-name-1', 'class-name-2'], ['class-name-3'])></div>
   *
   * @param {Styles} styles
   * @param {string[]} classNamesInModule css moduleのファイルにあるクラス名
   * @param {string[]} [additionalClassNames] css moduleのファイルにない単体のクラス名 linariaのcssで定義したものなど
   * @return {*}  {string}
   */
  const styleJoin = (
    classNamesInModule?: string[],
    additionalClassNames?: string[]
  ): string => {
    const moduleClassNames =
      cssModuleStyles && classNamesInModule && classNamesInModule.length
        ? classNamesInModule.map(className => cssModuleStyles[className])
        : []

    if (additionalClassNames) {
      return moduleClassNames.concat(additionalClassNames).join(' ')
    }

    return moduleClassNames.join(' ')
  }

  /**
   * linariaのcss prop結合用
   *
   * @param {string[]} linariaCss
   * @return {*}  {string}
   */
  const styleJoinLinaria = (linariaCss: string[]): string =>
    styleJoin([], linariaCss)

  return { styleJoin, styleJoinLinaria } as const
}

つかってみる

CSS ModulesとLinariaどちらも使用するコンポーネントの場合↓

import styles from './index.module.scss'
import { useStyleJoin } from 'hooks/useStyleJoin'

const ExampleComponent = () => {  
  const { styleJoin } = useStyleJoin(styles)
  
  return(
    <div className={styleJoin(['classname-a', 'classname-b'], [linariaStyle1, linariaStyle2])}>
      {/* ... */}
    </div>
  )
}

const linariaStyle1 = css`...`

const linariaStyle2 = css`...`

Linariaのみの場合↓

import { css } from 'linaria'
import { useStyleJoin } from 'hooks/useStyleJoin'

const ExampleComponent = () => {  
  const { styleJoinLinaria } = useStyleJoin()
  
  return(
    <div className={styleJoinLinaria([linariaStyle1, linariaStyle2])}>
      {/* ... */}
    </div>
  )
}

const linariaStyle1 = css`...`

const linariaStyle2 = css`...`

おわりに

CSS ModulesとCSS-in-JSライブラリが入り混じった構成がどれほど一般的なのかわかりませんが、そのようなプロジェクトにおいては結構便利な方法なのではないかと思います。
また、今回はLinariaでしたが、Emotionなど他のCSS-in-JSライブラリの場合でも応用が効くかもしれません。
CSS ModulesかCSS-in-JSかどっちかに統一しろよという意見があるかもしれませんが、それは全くもってその通りだと思います……。

Discussion