Headless Component開発をはじめよう (Headless UI + React Spectrum)
はじめに
この記事ではライブラリを活用したHeadlessなReact Component開発について紹介します。
Not Headless Component
Headless Component の紹介の前にHeadless ComponentではないComponentとはなんでしょうか。
ReactでComponent を作成する際に
Material-UIやAnt Designを使ったことがある人も多いのでは多いのではないでしょうか。
これらのライブラリは<Button />
や<Menu />
といったスタイル付属のReactコンポーネント集になっています。
自前でスタイルを書かずに使えるので便利ではあるのですが以下のような欠点があります。
細かい見た目の調整が難しい。
ライブラリにもよるのですが、細かい調整が難しいものが多いです。
例えばAnt DesignのButtonコンポーネントのAPIはこちらになります。
5種類の主なボタンタイプと他4つの追加プロパティを提供しています。他にもshape
なども指定できますが、どうしても細かいカスタマイズはできません。どうしてもそのライブラリ特有の印象が出てしまいます。
ライブラリのスタイルのビルドについて注意する必要がある。
ライブラリがどういった形でスタイルシートを配布しているのかによって対応が変わります。
例えばメインのアプリケーションは全てstyled-components
でスタイルを書いているのにも関わらずライブラリが.css
形式でしかスタイルを配布していない場合にビルドチェーンに手を加える必要があるかもしれません。
筆者は最近Reactアプリケーションを作る際にはNext.jsを利用することが多いのですが、公式のwith-ant-designの例などを見ると少々癖のある設定が必要なことがわかります。
Headless Component
そもそも我々はスタイルが欲しかったのでしょうか。振る舞い(DOM構造やイベント制御)が欲しかっただけでスタイルは自分で制御したいのかもしれません。
これはHeadless Componentによって解決することができます。Headless Componentとは一般的にはスタイルを持たないComponentのことを指します。
Headless UI
実際にこの思想で作られているHeadless UIを見てみましょう。
Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.
とスタイルが無いことが最初に書かれていますね。Tailwind CSS という単語が出てきていますがこれはTailwind Labsの元で作られているからです。相性はとても良いと感じますが必ずしもTailwind CSSを一緒に使う必要はありません。
実際にコンポーネントを見てみましょう。React用のコンポーネントはpackages/@headlessui-react
にあります。説明のためTransition
に注目します。
import { Transition } from '@headlessui/react'
import { useState } from 'react'
function MyComponent() {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
<Transition
show={isOpen}
enter="transition-opacity duration-75"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
I will fade in and out
</Transition>
</>
)
}
TransitionのPropsに渡しているenter
やleave
に注目してください。Tailwind CSSのクラス名となっています。コンポーネントに状態変化が発生した際にどのようなスタイルを付与するかコンポーネントマウント側からクラス名を指定する形をとっています。
これまでのコンポーネントはdisabled
というフラグを渡したらDOMのdisabledを付与をさせるとと同時にスタイルもグレー色などに変えるものが多かったです。対してHeadless UIは見た目に関して専用のpropsを受け取るようになっています。
React Spectrum (Stately + Aria)
Adobe社が管理しているライブラリにReact Spectrum (Stately + Aria)があります。
A collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.
少し紛らわしいのですがReact Spectrumというプロジェクト存在しており、React Spectrum, React Stately, React Ariaの3つのライブラリが配布されています。
React Spectrum は内部でReact StatelyとReact Ariaを利用してスタイルを付与しています。
そのためHeadless Component開発という文脈ではReact Stately + React Ariaの2つを活用します。
React StatelyとReact Ariaの特徴はほぼ全てがHooksベースということです。
実際にReact Stately と React Aria を使用した例を見てみましょう。
import {useRadioGroupState} from '@react-stately/radio';
let RadioContext = React.createContext();
function RadioGroup(props) {
let {children, label} = props;
let state = useRadioGroupState(props);
let {radioGroupProps, labelProps} = useRadioGroup(props, state);
return (
<div {...radioGroupProps}>
<span {...labelProps}>{label}</span>
<RadioContext.Provider value={state}>{children}</RadioContext.Provider>
</div>
);
}
function Radio(props) {
let {children} = props;
let state = React.useContext(RadioContext);
let {inputProps} = useRadio(props, state);
return (
<label style={{display: 'block'}}>
<input {...inputProps} />
{children}
</label>
);
}
<RadioGroup label="Favorite pet">
<Radio value="dogs">Dogs</Radio>
<Radio value="cats">Cats</Radio>
</RadioGroup>
useRadioGroupState
が状態管理に使われています。
一方useRadioGroup
やuseRadio
の返り値は対応するDOMに展開されています。
例ではdivに直接スタイルを書いていますがpropsで受け取るようにすればHeadlessなコンポーネントが作成できます。
実際に開発する際にはView層から直接これらのHooksを呼び出さず、スタイルを指定するためのPropsを受け取るコンポーネントを作成して呼び出すことが多いでしょう。
Headless UI と React Stately + Aria
2つのライブラリを見てきましたが筆者は異なる印象をうけました。
大雑把に評すると
- React Stately + Ariaはカスタマイズが効くけど難しそう
- Headless UIはカスタマイズが難しいけど簡単そう
React SpectrumのメンテナであるDevon Govett氏はこんなツイートをしています。
つまり自由度のためにあえて低レベルな設計にしているということですね。
render-props や component based APIを好むならReact Stately + Aria を使って自前で作ればよいということです。その逆はできません。上手くHooksを活用して機能のみに焦点を当てている設計になっています。
どっちがいいの?
適材適所。
私は業務で導入していますがHeadless UIはまだ数が少ないのでReact Stately + Ariaを使うことが多いです。この時、Headless UIの内部実装を見ながら「こうするとスタイルの指定がしやすそうだな」と参考にしながらPropsのインターフェースを決定しています。
Headless UIのほうが試しやすいでしょう。
React Stately + Aria は自由度が高いぶん最初は苦戦するかもしれません。
(大雑把ですが)DOMに渡せるものをあらゆるものを受け取ってmergeProps
やfilterDOMProps
というutilな関数を通すという内部実装になっています。
実装に困った時にはSpectrum Packageの実装を参考にするとよいでしょう。公式のReact Stately + Ariaの使用例といえます。
スタイルの自由度が高すぎて破綻しない?
実際の運用ではHeadlessなComponentを作って、更に取りうるスタイルを明示したラッパーComponentを作り、View層から読み込むこともありでしょう。
type Props = {
type?: "primary" | "secondary";
};
export const Button = (props: Props) => {
return (
<HeadlessButton className={props.type ? `btn btn--${props.type}` : "btn"}>
{"FooBar"}
</HeadlessButton>
);
};
比較表
Headless UI | React Stately + Aria | |
---|---|---|
管理者 | Tailwind Labs | Adobe |
形式 | Components | Hooks |
ライセンス | MIT | Apache License 2.0 |
TypeScript | ◯("strict": true) | ◯("strict": false) |
別に新しい概念ではないのでは
はい。
Hooksの導入(だいぶ前ですが)やデザインシステムの流行りからこうした考えに改めて焦点があっているのではという考えです。
Discussion