noah.plus

Renderless Componentでさらにコンポーネントの再利用性を高める(Vue.js)

2018-11-12

Renderless Componentとは、その名前の通り「表示するもの」を持たないコンポーネントのこと。Vue.jsの単一ファイルコンポーネントに当てはめると以下のようになる。

  • template → なし
  • script → あり
  • style → なし

つまりHTMLやCSSはなくなり、dataやmethodなどJavaScriptの部分だけが残る。

Scoped SlotとRenderless Componentの比較

前回の記事で作成した、Scoped Slotを使ったコンポーネントをRenderless Componentsに書き換えてみる。

前回の記事:Scoped Slot でコンポーネントの再利用性を高める(Vue.js)

「動作は同じだがレイアウトが違う」というコンポーネントを作成するときには、Scoped Slotを使うのも1つの手だが、Rendeless Componentの方がよりスッキリとした書き方ができる。

ToDo.vue(Scoped Slot)

<template>
  <div class="wrapper">
    <slot :todos="todos" :toggle="toggleDone"></slot>
  </div>
</template>

<script>
export default {
  name: "ToDo",
  data() {
    return {
      todos: [
        {title: 'ブログ書く(1本目)', isDone: false},
        {title: 'ブログ書く(2本目)', isDone: false},
        {title: 'ブログ書く(3本目)', isDone: false},
        {title: 'ブログ書く(4本目)', isDone: false}
      ]
    };
  },
  methods: {
    toggleDone (todo) {
      todo.isDone = !todo.isDone
    }
  }
};
</script>

<style scoped>
.wrapper {
  text-align: center;
}
</style>

ToDo.vue(Renderless Component)

<script>
export default {
  name: "ToDo",
  data() {
    return {
      todos: [
        { title: "ブログ書く(1本目)", isDone: false },
        { title: "ブログ書く(2本目)", isDone: false },
        { title: "ブログ書く(3本目)", isDone: false },
        { title: "ブログ書く(4本目)", isDone: false }
      ]
    };
  },
  methods: {
    toggleDone(todo) {
      todo.isDone = !todo.isDone;
    }
  },
  render() {
    return this.$scopedSlots.default({
      todos: this.todos,
      toggle: this.toggleDone
    });
  }
};
</script>

render() メソッド

ToDo.vueをScoped SlotからRenderless Componentに書き換えても、機能的には差がない。しかしRenderless Componentの方には render() という見慣れぬメソッドが存在している。急にReactっぽい。

render() メソッドを使うことで、templateをJavaScript側からプログラマティックに操作することができる。ちなみにrender() メソッドではJSXを扱うこともできる。

描画関数とJSX

コンポーネントのdataやpropsなど、諸々のデータを取得したいときは、Vue.jsのインスタンスプロパティAPIからアクセスする。

render() {
  return this.$slots.default
}

例えば上記のように書けば、デフォルトスロットに割り当てられたテンプレートを描画することができる。

  render() {
    return this.$scopedSlots.default({
      todos: this.todos,
      toggle: this.toggleDone
    });
  }

今回の例はScoped Slotなので、this.$scopedSlotsを使う。Scoped Slotの場合、引数としてバインドするプロパティを渡すことができる。

    <to-do>
      <div slot-scope="{ todos, toggle }">
        <div v-for="todo in todos" :key="todo.title">
          <span :style="todo.isDone ? { opacity: 0.2 } : {}">{{
            todo.title
          }}</span>
          <button @click="toggle(todo);">&times;</button>
        </div>
      </div>
    </to-do>

親コンポーネントからの呼び出し方は変化なし。普通のScoped Slotを持つコンポーネントと同様に子コンポーネントでバインドしたプロパティにアクセスできる。

まとめ

render() メソッドを使ったRenderless Componentは、Vue.jsの世界ではあまり見慣れぬ書き方でちょっと戸惑う。しかし賢く使えればコンポーネントのシンプルさを保ちつつ、パワフルな機能の実装ができそう。

いろいろ調べている間に見かけた、一連のHTTP通信の処理(CRUD、通信中判定、エラーハンドリング)を集約したRenderless Componentとかすごい便利そう。(Apollo Clientとかまさにこれ?)

Building Renderless Components to Handle CRUD Operations in Vue.js

Renderless Component、すごく強力だけど雑に使うと遠からず自分の首を絞めることになりそうなので、計画的に使っていきたい。

参考


noah.plus