👻

【Vue】Renderless Componentsを使ってみる

3 min read

この記事では、Vue.jsでたま〜に使うRenderless Componentsをどんな感じに使っているのか紹介しています。
なお、Vue2/Composition APIで書いています。

Renderless Componentsとは?

レンダリングすべきDOMを持ってないコンポーネントのことです。
こんな感じで作ります。

<script lang="ts">

export default defineComponent({
  setup() {
    return () => { }
  },
});

</script>

templateは不要です。いきなりscriptをSFCの中で書いていきます。
setup関数でreturn ()=>{ }とすると、本来return ()=>h("div")のようにレンダリング関数を返すパターンにもかかわらず何も返さないという動きになります。
この場合、DOMが何も作られません。

使用例

Renderless Componentsは、DOMとしては何も作らないけど、実際には画面上に何かをレンダリングするコンポーネントや、空divもなにも出したくないというようなケースに使うとよいんじゃないかと思います。

DOMはいらないけど画面上に何かをレンダリング

OpenLayersという地図ライブラリでの使用例を紹介します。
OpenLayersではJavaScript内で指定したレイヤの設定内容に応じて、canvas等に地図を描画してくれます。
JavaScriptで書くとこんな感じになります。

const map = new Map({/* 地図全体の設定いろいろ */})
const layer = new Layer({/* レイヤの設定いろいろ */})

map.addLayer(layer) //地図にレイヤを追加
map.removeLayer(layer) //地図からレイヤを削除

いちいちaddLayerとかを呼び出して制御するのは、だいぶめんどくさいので、Vue内でのコンポーネントのライフサイクルと、レイヤの追加削除を同期させると、直観的でいい感じになりそうです。
これはレイヤを表現できるコンポーネントを作ることで実現していきます。
でもレイヤを表示させるときに余計なDOMは作りたくないので、Renderless Componentsでやってみます。

<script lang="ts">
import TileLayer from "ol/layer/Tile";
import { Map } from "ol";
import { OSM } from "ol/source";

export default defineComponent({
  setup() {
    const map: Map | undefined = inject("Map") //親コンポーネントから地図を取得

    const layer = new TileLayer({
        source: new OSM()
      })) //レイヤの設定

    onMounted(() => {
      map?.addLayer(layer) //地図にレイヤを追加
    })
    onUnmounted(() => {
      map?.removeLayer(layer) //地図からレイヤを削除
    })
    return () => { }
  },
});
</script>

とりあえず、地図のインスタンス(Map)は親コンポーネントからprovide/injectされる想定で記載しています。

さらに、このRenderless Componentを使う例を書いてみます。

<template>
  <div class="Container">
    <div class="mapLayerControlContainer">
      <input v-model="selected" type="radio" name="OSM" value="OSM" />
      <label for="OSM">OpenStreetMap</label>
      <input v-model="selected" type="radio" name="GSI" value="GSI" />
      <label for="GSI">国土地理院 標準地図</label>
    </div>
    <ol-map class="mapContainer">
      <ol-osm-tile-layer v-if="selected === 'OSM'"></ol-osm-tile-layer>
      <ol-gsi-tile-layer v-else-if="selected === 'GSI'"></ol-gsi-tile-layer>
    </ol-map>
  </div>
</template>

inputで設定しているselectedという値に応じて、ol-osm-tile-layerol-gsi-tile-layerが出たり消えたりするイメージです。

ol-osm-tile-layerol-gsi-tile-layerは、Renderless Componentsになっています。v-if等を使ったコンポーネントのライフサイクルに反応して、レイヤが出たり消えたりしてくれるので、OpenLayersの知識がなくとも、何をやっているのかぱっと見で理解しやすい形になっているのではないかと思います。当然ですがv-showでは動かないので、注意が必要です。

いきなりDefault Slotを出したい

空divを作らずにいきなりDefault Slotを出したい場合こんな感じになります。

setup(_, ctx) {
    const slot = ctx.slots.default
    if (slot)
      return () => slot()
    return () => { }
}

provide/injectの関係で子コンポーネントデフォルトスロットは受け付けるようにしておきたいけど、DOMは何にもいらないんだよな、という場合に使うことがあります。
でも空div作ったほうが分かりやすいこともあるんじゃないかと思います。

OpenLayersの例で言えば、地図の状態をまとめているMapのインスタンスを子コンポーネントにprovideするためだけのコンポーネントに使ったりします。

最後に

このやり方がどの程度正しいのか不安な部分が多いため、随時コメント等で教えていただければたいへん助かります。

Discussion

ログインするとコメントできます