ここではStimulus controllerとReact等におけるコンポーネントの違いについて説明します。
Reactのコンポーネントの特徴は、HTML(DOM)とイベント処理(JavaScript)を密結合させていることです。さらにCSSモジュールはCSS-in-JSを使用すると、CSSまでも密結合されます。全てを一緒にすることでコンポーネントとしての再利用性が高まるという考え方です。
それに対してStimulusはHTMLとイベント処理を独立させています。もちろんCSSも独立です。これは従来からのウェブフロントエンドの考え方でプログレッシブエンハンスメントと呼ばれるものです。独立させることでイベント処理単体の再利用性、もしくはCSS単体の再利用性が高まります。
どちらが正しくてどちらが誤っているかという単純なことは言えませんが、有利不利はあります。また最近ReactではRadix UI, React Aria Components, Base UI, Headless UI, Ark UIなどの"headless UI"が主流になってきており、「動作」と「表示」を分ける動きも増えています。
Reactのコンポーネントは以下の全てを行います。
それに対してStimulus controllerは既存のHTMLに対して動作を付与するものであり、DOMのレンダリングを行いません。
ReactのコンポーネントとStimulus controllerを比較するために、トグルを双方で実装した例を見ていきます。
useState)およびイベントハンドラ(clickHandler())も含まれています。app/views/components/toggle_stimulus.html.erb)が担当しています。app/javascript/controllers/switch_controller.js)ではステートを管理し(今回はaria-checked属性をステートとしている)、イベントハンドラ(toggle())が含まれています。data-controller="switch"やdata-action="click->switch#toggle..."が接続の役割を担います。export function TogglePlain() { const [enabled, setEnabled] = React.useState(false) function clickHandler() { setEnabled(!enabled) } return ( <button type="button" className="group bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2 aria-checked:bg-indigo-600" role="switch" tabIndex={0} aria-checked={enabled ? "true" : "false"} onClick={clickHandler} > <span className="sr-only">Use setting</span> <span aria-hidden="true" className="translate-x-0 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out group-aria-checked:translate-x-5" ></span> </button> ) }
<div class="text-center"> <button type="button" class="group bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2 aria-checked:bg-indigo-600" role="switch" tabindex="0" aria-checked="false" data-controller="switch" data-action="click->switch#toggle keydown.space:stop:prevent->switch#toggle" > <span class="sr-only">Use setting</span> <!-- Enabled: "translate-x-5", Not Enabled: "translate-x-0" --> <span aria-hidden="true" class="translate-x-0 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out group-aria-checked:translate-x-5" ></span> </button> </div>
import { Controller } from "@hotwired/stimulus" // Connects to data-controller="switch" export default class extends Controller { connect() { } toggle() { this.element.ariaChecked = this.element.ariaChecked === "true" ? "false" : "true" } }
<button>タグではないものにdata-controller="switch"をつけても正常に動作します。一方でReactコンポーネントの場合は<button>タグ以外のものでは使用できません。className を変更できるようにTogglePlainコンポーネントのpropsを工夫する必要があります。このようにStimulus controllerは動作のみを担当し、表示には関与しないため、異なるHTML要素を使ったり、CSSを大幅に変えたり、HTMLの構造自体を変更しても同じStimulus controllerで対応できます。この点においてはStimulus controllerの方がReactよりも再利用性が高いと言えます。