45
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

HRBrainAdvent Calendar 2022

Day 6

スタイルを上書きできるコンポーネントはなるべく作らない方がいいという話

Last updated at Posted at 2022-12-05

はじめに

この記事はHRBrain Advent Calendar 2022カレンダー2の6日目の記事です。

こんにちは、フロントエンドエンジニアの村崎です。
Amazonのブラックフライデーでルンバを買いました。

この記事では、スタイルを上書きできるコンポーネントの説明と、なぜなるべく作らない方がいいのかを書きます。

前提

この記事中でのスタイリングにはstyled-componentsを用います。
CSS in JSを前提としますが、別ライブラリやCSS modulesによるスタイリングでも共通項があると思います。

スタイルを上書きできるコンポーネントとは?

styled-componentsでは以下のようにすることで、スタイルの上書きを許容するコンポーネントを作ることができます。
classNameをpropから渡せるようにすると、styled-componentsがstyleを注入できるようになりoverrideができるようになります。1

marginをコンポーネント自身に持たせたりせず、呼び出し側で管理したい場合などによくこのように実装されているのを見かけます。

import styled from "styled-components"
// 親コンポーネント.tsx
const ParentPage = () => {
    return (
        <div>
            {...}
            <Component />
        </div>
    )
}

const Component = styled(AnyComponent)`
    margin-top: 16px;
`

// 子コンポーネント.tsx
type Props = {
    className?: string
}
const AnyComponent = (props: Props) => {
    return <div className={props.className}>{...}</div>
}

なぜ作らないほうがいいのか

margin以外にもあらゆるCSSが書けてしまうからです。

上記の例ではmargin-topを指定しました。
一見良さそうに見えますが、例えば以下のような子結合子を用いたセレクタによるoverrideを見てみます。

const Wrap = styled(AnyComponent)`
    margin-top: 16px;
    & > div:first-child {
        text-align: center;
    }
`

コンポーネントのがわだけではなく、中身のスタイルまで変更されてしまいました。
元のAnyComponentは自身がどこで使われているかを知り得ないため、コンポーネントの改修などでdivではなくspanなどに変える事がある場合、知らぬ所でスタイルが崩れます...。

さらに、AnyComponentが必ずしもトップレベルの要素にclassNameを渡しているとは限りません。
なんかよくわからないけど消したら表示が崩れるかも...というCSSの完成です。

実際、親要素で余白を付与したいためだけにclassNameを受け取るようにしたのに、別の画面でも再利用され、その際にかなりスタイルをいじられてしまった...という場面に遭遇することがあります。

じゃあclassNameなんて受け取らずにmarginを指定するpropにしたらいいじゃん!と思うかもしれませんが、全コンポーネントにそんなpropを生やすなんてことはしたくありません。

ではどうするか

スタイルに変化があるコンポーネントはpropsとして振る舞いを定義する

Polarisなどのデザインシステムのコンポーネントが参考になります。
コンポーネントのインターフェイスとして、スタイルの変化を制御させることで再利用性や保守性を高めることができます。

親要素でのmarginの付与にはレイアウトコンポーネントを使う

レイアウトコンポーネントとは、「パーツのレイアウトや間の余白」に責務を持つコンポーネントのことです。

ChakraUIMantineといったUIライブラリでこの責務を持つコンポーネント達がレイアウトとグルーピングされているため、レイアウトコンポーネントとこの記事では呼ぶことにします。

これらのライブラリでは、BoxFlexStackといったレイアウトコンポーネントを提供しています。
大変使い勝手が良いですが、これらのためにUIライブラリの導入はできない場合があります。
そのため、自前で実装してみます。

簡易的な実装例

import styled from "styled-components"

type StyleProps = {
    gap?: 4 | 8 | 12 | 16 | 24
    between?: boolean
    wrap?: boolean
}

const Wrap = styled.div<StyleProps>`
    display: flex;
    align-items: center;
    ${(props) => props.gap && `gap: ${props.gap}px`};
    ${(props) => props.between && `justify-content: space-between`};
    ${(props) => props.wrap && `flex-wrap: wrap`};
`

type Props = {
    children?: ReactNode
} & StyleProps

export const Stack = (props: Props) => {
    return (
        <Wrap
            gap={props.gap}
            between={props.between}
            grow={props.grow}
        >
            {props.children}
        </Wrap>
    )
}

サンプル

See the Pen Untitled by Murasaki-1102 (@murasaki-1102) on CodePen.

レイアウトコンポーネントを使いやすくするためのtips

必要最低限のpropsにする

classNameを継承させないことにより、propsからでしかスタイルの変更ができないようにしています。
これにより、限られたパターンでのスタイリングを制限できます。

プロパティの値をユースケースで縛る

例えばgapの値はデザインシステムなどの値に合わせnumberのリテラル型にすることで、型レベルで使用を制限できます。
flex-wrapのようなプロパティはwrap-reverseのユースケースがない場合、trueならwrapといったbooleanの型にしてしまうことでシンプルになり、他のプロパティとの衝突を避けることができます。

as propを用いてelementを指定できるようにする

サンプルでは実装していませんが、例えばli要素でflexなどをしたかった場合に、as="li"のようなas propを用いるとdomのネストが一段低くなります。2

レイアウトコンポーネントのメリットとデメリット

メリット

CSSを書かなくてよくなる

CSSの事を意識することなく、渡すpropsのことだけ考えればよくなります。
これは初学者やバックエンドエンジニアの方が多くフロントエンドを触るようなチームで有効です。
また、記述量が少なくなるため開発スピードも上がります。

スタイルの統一

サンプルではflexとgapで要素間の余白を実現しました。
このコンポーネントを使うだけで、意図しないgridやmarginの付与を防ぐことができます。治安が良い。
レビューもコンポーネントと渡しているpropsを見るだけなのでコストが低いです。

デメリット

要素が増える

divタグが増えることによるパフォーマンス懸念があります。これが最大のデメリットであり好き嫌いが別れる部分だと思います。

こちらはuhyoさんの記事の計測が参考になります。

DOMレベルでのパフォーマンスチューニングが必要なプロジェクトではレイアウトコンポーネントは採用できない可能性があります。

プロジェクトが大きくなると変更が大変

どのコンポーネントにも言えることですが、使われる箇所が多いほど影響範囲が大きくなります。
変更を感知するために自動テストの仕組みが必要になるでしょう。

終わりに

できるだけスタイルの上書きが可能なコンポーネントを作らないことでCSSの治安を守ることができます。

弊社HRBrainではコンポーネント設計やスタイリングの議論を共にできる仲間を募集しています。
興味を持った方がいればぜひこちらからご応募ください!

  1. https://styled-components.com/docs/basics#styling-any-component

  2. https://styled-components.com/docs/basics#extending-styles

45
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?