mcpField
R
renderless-components

renderless-components

github.com/patternsdev開発257185 回インストール28 週間前に更新
リポジトリ

renderless-components について

コンポーネントのロジックを再利用しながら、表示方法は親側で決めたい Vue 開発者向けのパターン解説です。スコープ付きスロットを使うレンダーレスコンポーネントと、コンポーザブルがより簡潔な場合を説明します。

主な機能

  1. 01コンポーネントのロジックと表示を分離
  2. 02スコープ付きスロットでデータとメソッドを提供
  3. 03親コンポーネントが描画内容を決定
  4. 04レンダーレスコンポーネントとコンポーザブルを比較

活用シーン

  1. 01異なる UI デザイン間でトグル動作を再利用
  2. 02コンポーネントライブラリ向けのコンポーネント API 作成
  3. 03テンプレートレベルでの UI 合成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/patternsdev/skills --skill renderless-components

インストール後、Claude Code で/renderless-componentsを入力して読み込まれたか確認します。

詳細

レンダーレスコンポーネント

目次

レンダーレスコンポーネントは、Vue でコンポーネントのロジックとプレゼンテーションを分離するためのパターンです。このパターンを使うと、_コンポーネントの視覚的な表現を規定せずに_機能をカプセル化できます。つまり、レンダーレスコンポーネントはロジックと動作だけに集中し、レンダリングは親コンポーネントに任せます。

レンダーレスコンポーネントは、異なる UI 実装に適用できる再利用可能なロジックを作成する必要がある場合に特に便利です。ロジックをレンダーレスコンポーネントに抽象化すると、コードを重複させずにさまざまな場面で簡単に再利用できます。

使う場面

  • 見た目がまったく異なるコンポーネント間でロジックを再利用する必要がある場合に使います
  • コンポーネントライブラリでコンポーネントベースの API を提供する際に役立ちます

使わない場面

  • コンポーザブルを使えば、コンポーネントのネストを増やさずに同じロジックを再利用できる場合(Vue 3 以降)
  • レンダーレスコンポーネントが、単純な関数やコンポーザブルのほうが明確に処理できる簡単なロジックをラップしている場合
  • スコープ付きスロット API が、コンポーザブルから直接返される値より理解しにくくなる場合

手順

  • スコープ付きスロットプロパティを使って、単一の <slot> からデータとメソッドを提供するコンポーネントを作成します
  • 親コンポーネントで v-slot の分割代入を使い、提供されたデータとメソッドにアクセスします
  • コンポーネントのネストが増えるのを避けるため、Vue 3 ではレンダーレスコンポーネントよりコンポーザブルを優先します
  • テンプレートレベルの合成や、コンポーネントベースの API が必要な場合はレンダーレスコンポーネントを使います

詳細

トグル、トグル、トグル

アプリケーションのさまざまな箇所で使うトグル UI 要素があり、インスタンスごとに見た目が異なる場合を考えてみましょう。ボタンとして表示されるトグルもあれば、チェックボックスやスイッチとして表示されるものもあります。

3 つの異なるトグルコンポーネントを作成することもできますが、どのトグル要素も同じロジックと動作を持つことに気づきます。各トグルには非アクティブ状態とアクティブ状態があり、コンポーネントのデータプロパティ(例:checked)で管理されます。トグルをクリックすると、コンポーネントの状態が非アクティブからアクティブに、またはその逆に切り替わります(つまり、checked = !checked)。

共通のロジックと動作を抽出すれば、個々のトグルコンポーネントで状態や切り替えメソッドを何度も定義せずに済み、より再利用しやすいパターンを作れることがすぐにわかります。コンポーザブルを使うと、異なるトグルコンポーネント間で状態を持つ共通ロジックをカプセル化して共有できるため、これはコンポーザブルを使うのに最適なケースです。

useCheckboxToggle:

import { ref } from "vue";

export function useCheckboxToggle() {
  const checkbox = ref(false);

  const toggleCheckbox = () => {
    checkbox.value = !checkbox.value;
  };

  return {
    checkbox,
    toggleCheckbox,
  };
}

このコンポーザブルを使えば、さまざまなトグルコンポーネントで useCheckboxToggle() 関数を使い、共通の状態と切り替えロジックを共有できます。

ただし、Vue のスロット機能を活用する別の方法もあります。それがレンダーレスコンポーネントパターンです。

レンダーレスコンポーネント

Vue のレンダーレスコンポーネントとは、ロジックをカプセル化し、独自のマークアップをレンダリングせずにスコープ付きスロットを介して子にデータを提供するコンポーネントです。データの表示方法は親コンポーネントが決めます。

シンプルなレンダーレスの Toggle コンポーネントを見てみましょう。

<script setup>
  import { ref } from "vue";

  const checked = ref(false);

  const toggle = () => {
    checked.value = !checked.value;
  };
</script>

<template>
  <slot :checked="checked" :toggle="toggle"></slot>
</template>

Toggle コンポーネントは、独自の HTML をレンダリングしません。スコープ付きスロットを通じて、データ(checked と toggle)を提供するだけです。親コンポーネントはこのデータを使い、好きな UI をレンダリングできます。

レンダーレスの Toggle をボタンとして使う場合:

<template>
  <Toggle v-slot="{ checked, toggle }">
    <button @click="toggle">
      {{ checked ? "ON" : "OFF" }}
    </button>
  </Toggle>
</template>

レンダーレスの Toggle をチェックボックスとして使う場合:

<template>
  <Toggle v-slot="{ checked, toggle }">
    <label>
      <input type="checkbox" :checked="checked" @change="toggle" />
      {{ checked ? "Checked" : "Unchecked" }}
    </label>
  </Toggle>
</template>

レンダーレスの Toggle をスイッチとして使う場合:

<template>
  <Toggle v-slot="{ checked, toggle }">
    <div
      class="switch"
      :class="{ active: checked }"
      @click="toggle"
    >
      <div class="switch-handle"></div>
    </div>
  </Toggle>
</template>

いずれの場合も、同じレンダーレスの Toggle コンポーネントがトグルのロジックを提供しますが、レンダリング方法はまったく異なります!

コンポーザブルとレンダーレスコンポーネント

コンポーザブルもレンダーレスコンポーネントも、コンポーネント間でロジックを再利用するという目的を実現します。ただし、いくつか違いがあります。

コンポーザブル:

  • ロジックは通常の JavaScript 関数にカプセル化されます。
  • <script setup> または setup() で直接使用できます。
  • 追加のコンポーネントレイヤーは必要ありません。

レンダーレスコンポーネント:

  • ロジックは Vue コンポーネントにカプセル化されます。
  • スコープ付きスロットを使用して、データを子コンポーネントに渡します。
  • テンプレートにコンポーネントレイヤーが追加されます。

一般に、コンポーザブルはコンポーネントの入れ子を増やさないため、Vue 3 では推奨される方法です。ただし、コンポーネントベースの API(たとえばコンポーネントライブラリ)を提供したい場合や、テンプレートレベルのコンポジションが必要な場合は、レンダーレスコンポーネントが役立ちます。

ソース

参考資料

コメント