StyleX で考える
基本原則
StyleX の存在の理由やその決定の背後にある理由を理解するためには、それを導く基本原則に精通することが有益です。これにより、StyleX が自分に合ったソリューションであるかどうかを判断するのに役立ちます。
これらの原則は、StyleX に新しい API を設計するときにも役立てることができます。
同居
DRY コードのメリットはありますが、スタイルを作成する場合通常はそれが当てはまらないと考えています。スタイルを記述する最もよくて最も読みやすい方法は、マークアップと同じファイルに記述することです。
StyleX は、スタイルのローカルでの作成、適用、および推論を行うように設計されています。
決定論的解決
CSS は強力で表現力の豊かな言語です。ただし、ときには脆弱に感じることがあります。これは CSS の仕組みの理解不足に起因することもありますが、その多くは、異なる特異度を持つ CSS セレクタが競合しないようにする規律と整理に起因しています。
この問題に対する既存のソリューションの大部分は、規則と慣習に依存しています。
BEM および OOCSS の慣習
BEM と OOCSS はこれらの問題を回避する命名規則を導入します。開発者が規則を一貫して遵守し、頻繁にスタイルの統合を回避します。これにより、CSS が膨大になる可能性があります。ユーティリティクラス
Tailwind CSS や Tachyons などのアトミックユーティリティクラス名は、慣習とリントルールに依存して、競合するクラス名が同じ要素に適用されないようにしています。そのようなツールは、スタイルを適用できる場所と方法に制約を追加し、スタイリングにアーキテクチャ上の制限を課します。StyleX は、スタイルの整合性と予測可能性と利用可能な表現力の両方を向上させることを目指しています。ビルドツールによってこれが可能になると考えています。
StyleX は、ファイル間で機能する、完全に予測可能で決定論的なスタイリングシステムを提供します。複数のセレクタをマージする場合だけでなく、複数の省略記法とロングハンドのプロパティをマージする場合にも確定的結果が得られます。(例: margin
と margin-top
)。
「適用された最後のスタイルが常に優先されます。」
低コストの抽象化
StyleX のパフォーマンスコストに関しては、StyleX が特定のパターンを実現するための最速の方法であるべきだという原則があります。一般的なパターンではランタイムコストが発生しないようにし、高度なパターンでは可能な限り高速にする必要があります。ランタイムパフォーマンスの向上と引き換えにより、ビルド時に多くの作業を行います。
実際の運用での実装方法を以下で説明します。
1. ローカルで作成して適用されるスタイル
同一ファイル内でのスタイルの記述および使用時は、StyleX のコストは 0 になります。これは、stylex.create
呼び出しをコンパイルして削除するだけでなく、可能な場合は stylex.props
呼び出しも StyleX がコンパイルして削除するためです。
つまり
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
red: {color: 'red'},
});
let a = stylex.props(styles.red);
次のようにコンパイルされます。
- JS 出力
- CSS 出力
import * as stylex from '@stylexjs/stylex';
let a = {className: 'x1e2nbdu'};
.x1e2nbdu { color: red; }
ここではランタイムのオーバーヘッドはありません。
2. ファイル全体で使用されるスタイル
ファイルの境界をまたいでスタイルを渡すことは、追加のパワーと表現力のためにわずかなコストがかかります。stylex.create
呼び出しは完全に削除されず、代わりにクラス名をキーにマッピングしたオブジェクトを残します。stylex.props()
呼び出しはランタイムで実行されます。
たとえば、このコードでは
import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
foo: {
color: 'red',
},
bar: {
backgroundColor: 'blue',
},
});
function MyComponent({style}) {
return <div {...stylex.props(styles.foo, styles.bar, style)} />;
}
次のようにコンパイルされます。
- JS 出力
- CSS 出力
import * as stylex from '@stylexjs/stylex';
const styles = {
foo: {
color: 'x1e2nbdu',
$$css: true,
},
bar: {
backgroundColor: 'x1t391ir',
$$css: true,
},
};
function MyComponent({style}) {
return <div {...stylex.props(styles.foo, styles.bar, style)} />;
}
.x1e2nbdu { color: red; }
.x1t391ir { background-color: blue; }
少しコードが増えますが、stylex.props()
関数の高速性により、ランタイムコストは依然として最小限に抑えられています。
他のほとんどのスタイル設定ソリューションでは、ファイルの境界をまたいでのスタイルの合成はできません。最新の方法は、クラス名のリストを結合することです。
小さな API サーフェイス
私たちの目標は、StyleX を可能な限り最小限で学習しやすいものにすることです。そのため、API をあまり多く作成したくありません。代わりに、可能な限り一般的な JavaScript のパターンに依存して、可能な限り最小の API サーフェイスを提供したいと考えています。
StyleX のコアは、2 つの関数に集約できます。
stylex.create
stylex.props
stylex.create
はスタイルを作成するために使用され、stylex.props
はこれらのスタイルを要素に適用するために使用されます。
これらの 2 つの関数では、StyleX 独自の API やパターンを導入するのではなく、一般的な JS パターンに依存することを選択しています。たとえば、条件付きスタイルの API はありません。代わりに、boolean 式または三項演算子を使用して条件付きでスタイルを適用できます。
JavaScript のオブジェクトや配列を扱うときには、期待どおりに動作する必要があります。予期せぬことはありません。
安全なスタイル入力
TypeScript は、提供するエクスペリエンスと安全性により非常に人気が高まっています。しかし、私たちのスタイルはほとんど型付けされておらず、信頼性に欠けます。Vanilla Extract などの革新的なプロジェクトを除いて、他のスタイリングソリューションではスタイルは単なる文字列の詰め合わせです。
StyleX は、強力な静的型付けを使用して Flow で記述されています。NPM のパッケージには、Flow と TypeScript の両方の自動生成型が含まれています。2 つの型システムの間に互換性がない場合は、時間をかけてカスタム TypeScript 型を作成し、元の Flow と同等のレベルのパワーと安全性を確保します。
すべてのスタイルには型が付けられています。プロパティとしてスタイルを受け入れる場合、型を使用して受け入れられるスタイルを制限できます。スタイルは他のコンポーネントプロパティと同じくらい安全な型でなければなりません。
StyleX API は堅牢に型付けされています。StyleX で定義されたスタイルも型付けされています。これは、生のスタイルのオーサリングに JavaScript オブジェクトを使用することで可能になりました。これは、テンプレート文字列ではなくオブジェクトを選択した大きな理由の 1 つです。
これらの型は、コンポーネントが受け入れるスタイルに対するコントラクトを設定するために活用できます。たとえば、コンポーネントのプロパティは、 color
と backgroundColor
のみを許可するように定義できますが、他のスタイルは許可しません。
import type {StyleXStyles} from '@stylexjs/stylex';
type Props = {
//...
style?: StyleXStyles<{color?: string; backgroundColor?: string}>;
//...
};
別の例では、プロパティはマージンを禁止できますが、他のすべてのスタイルは許可します。
import type {StyleXStylesWithout} from '@stylexjs/stylex';
type Props = {
//...
style?: StyleXStylesWithout<{
margin: unknown;
marginBlock: unknown;
marginInline: unknown;
marginTop: unknown;
marginBottom: unknown;
marginLeft: unknown;
marginRight: unknown;
marginBlockStart: unknown;
marginBlockEnd: unknown;
marginInlineStart: unknown;
marginInlineEnd: unknown;
}>;
//...
};
スタイルが型付けされることで、コンポーネントのスタイルをカスタマイズする方法に関する極めて洗練されたルールが、実行時コストなしで有効になります。
共有可能な定数
CSS クラス名、CSS 変数、その他の CSS 識別子はグローバル名前空間で定義されています。CSS 文字列を JavaScript に取り込むことは、型安全性と作成可能性を失うことを意味します。
私たちはスタイルを型安全にしたかったので、かなりの時間を費やして、これらの文字列を JavaScript 定数への参照に置き換える API を作成しました。これまで、これは次の API に反映されています。
stylex.create
生成されたクラス名を完全に抽象化します。表現するスタイルを示す「不透明」な強固な型の JavaScript オブジェクトを扱います。stylex.defineVars
生成された CSS 変数の名前を抽象化します。これらは定数としてインポートされ、スタイル内で直接使用できます。stylex.keyframes
キーフレームアニメーションの名前を抽象化します。代わりに、定数として宣言され、参照によって使用されます。
container-name
や @font-face
といった他の CSS 識別子を型安全にする方法を調べています。
フレームワークに依存しない
StyleX は CSS-in-JS ソリューションであり、CSS-in-React ソリューションではありません。StyleX は現在 React での動作に最適化されていますが、JavaScript でマークアップを作成できるフレームワークであればどれでも使用できます。これには、JSX、テンプレート文字列などを使用するフレームワークが含まれます。
stylex.props
は、className
と style
のプロパティを持つオブジェクトを返します。さまざまなフレームワークで機能させるために、この変換を行うためのラッパー関数が必要になる場合があります。
カプセル化
ある要素のすべてのスタイルは、その要素自体のクラス名によって引き起こされるものである必要があります。
CSS を使用すると、スタイルを「遠隔からのスタイル」を引き起こす方法で簡単に作成できます。
.className > *
.className ~ *
.className:hover > div:first-child
これらのパターンのすべては強力ですが、スタイルを不安定で予想しにくくします。ある要素にクラス名を適用すると、まったく別の要素に影響を与える可能性があります。
color
などの継承可能なスタイルは継承されますが、それが StyleX が許可するスタイルの唯一の形です。これらの場合でも、要素に直接適用されたスタイルは常に継承されたスタイルよりも優先されます。
複雑なセレクターを使用すると、通常はシンプルなクラスセレクターよりも複雑なセレクターの方が優先度が高くなるため、多くの場合これは当てはまりません。
StyleX はこの種類のセレクターをすべて許可しません。現在、このため特定の CSS パターンを StyleX を使用して表現することが不可能になっています。StyleX の目標は、スタイルのカプセル化を損なうことなく、これらのパターンをサポートすることです。
StyleX は CSS プリプロセッサではありません。高速で予測可能なシステムを構築するため、CSS セレクターの機能に意図的に制約を加えています。テンプレート文字列ではなく JavaScript オブジェクトに基づく API は、これらの制約を自然に感じさせるように設計されています。
簡潔さよりも可読性と保守性
最近登場したユーティリティベースのスタイルソリューションの中には、非常に簡潔で記述が簡単なものがあります。StyleX は簡潔さよりも可読性と保守性を優先しています。
StyleX は、可読性を優先し、学習曲線を緩やかにするために、おなじみの CSS プロパティ名を使用することを選択しています。(ただし利便性を重視して、ケバブケースの代わりにキャメルケースを使用しています)。
また、スタイルが使用される HTML 要素とは別にオブジェクト内で作成されるようにしています。この決定を行った理由は、HTML マークアップの可読性を向上させ、目的を示す適切な名前のスタイルにするためです。たとえば、styles.active
のような名前を使用することで、スタイルが適用される理由を明確にし、適用されるスタイルの内容を掘り下げる必要がなくなります。
この原則により、StyleX では他のソリューションと比較して、スタイルの作成に時間がかかる場合があります。
時間をかけて可読性を向上させるために、これらのコストは価値があると考えています。各 HTML 要素に意味のある名前を付けることで、スタイル自体よりも多くの情報を伝えることができます。
スタイルをインラインで使用するのではなく、スタイルへの参照を使用することの副次的な利点は、**テスト可能性** です。ユニットテスト環境では、StyleX はすべての atomic スタイルを削除し、スタイルそのものではなく、スタイルのソースの場所を示す単一のデバッグ用クラス名のみを出力するように構成できます。
他の利点として、スナップショットテストがより堅牢になります。スタイルが変更されても変更されないためです。
モジュール性とコンポーザビリティ
NPM によってプロジェクト間でコードを共有することが非常に簡単になりました。しかし、CSS の共有は依然として課題となっています。サードパーティのコンポーネントには、カスタマイズが困難または不可能なスタイルが組み込まれているか、まったくスタイルがありません。
予測可能な方法でパッケージ間でスタイルをマージおよびコンポーズするための適切なシステムがないことは、パッケージ内でスタイルを共有する場合の障害にもなってきました。
StyleX は、NPM 上のパッケージ内でコンポーネントと共にスタイルを簡単に確実に共有するためのシステムを作成することを目指しています。
グローバル構成を回避する
StyleX はプロジェクト間で似たような動作をする必要があります。StyleX の構文や動作を変更するプロジェクト固有の構成を作成することは避けてください。利便性を優先して、構成可能な一貫性を選択しました。Lint とタイプを利用してプロジェクト固有のルールを作成します。
また、プロジェクト内で特別な意味を持つマジック文字列も避けました。代わりに、すべてのスタイル、変数、共有定数は、一意の名前やプロジェクトの構成を必要とせず、JavaScript インポートです。
多数の小さなファイルより少ないファイル
大量の CSS を処理する場合、lazy-load CSS はページの最初のロード時間を高速化する方法です。しかし、更新時間が遅くなる、または Interaction to Next Paint (INP) メトリックを犠牲にします。ページの CSS を lazy-load すると、ページ全体のスタイルが再計算されます。
StyleX は、最適化された単一の CSS バンドルを最初にロードするように最適化されています。総 CSS 量が小さすぎて、すべての CSS をパフォーマンスに影響を与えずに最初にロードできるシステムを作成することを目指しています。
「クリティカル CSS」などの最初のロード時間を高速化するための他のテクニックは StyleX と互換性がありますが、通常は不要です。