Vue.jsにおけるRender PropとScoped Slotsについて

こんにちは。転職によりReact畑からVue畑に乗り換えることになったフロントエンドエンジニアです。

Vueでも描画関数使ってればRender Propsも使えるじゃんと思ったところ、Vue.js 作者のEvan You曰く「Render PropパターンはVue.jsにおけるScoped Slotsと同じ」とのこと。

本当にそうなのか簡単な実装例を用意してみました。

Render Prop

まず、そもそものRender Propを使う目的としては
「コードの再利用性を高めるための実装パターンで、複数のコンポーネントに適用したい汎用的な振る舞いを抽出すること」であると捉えています。

少し前にRender Prop使えばHOCいらんくね?という記事が話題になりましたが、HOCみたいにあるコンポーネントに特定の振る舞いを追加するのが目的と考えるとイメージしやすいと思います。

パターンの詳細についてですが、実装を見るのが一番早いと思うので、下記にVue.jsでの実装例を紹介したいと思います。
今回は例として、ありがちな「選択された要素だけスタイルを変える」という振る舞いを実装してみます。

App.vue
//「選択された要素だけスタイルを変える」振る舞いを持つコンポーネントです。
//このコンポーネントでは `render` propに渡された関数を実行することによって描画します。
const Selected = {
  props: {
    render: {
      default: h => null
    }
  },
  data() {
    return {
      selectedVal: 0
    };
  },
  methods: {
    select(value) {
      this.selectedVal = value;
    }
  },
  render() {
    return this.$props.render({
      selectedVal: this.selectedVal,
      select: this.select
    });
  }
};

export default {
  functional: true,
  render: (h, { props }) => (
    <div>
      <Selected
        render={({ selectedVal, select }) => (
          <div>
            <input
              type="number"
              onChange={event => select(event.target.value)}
              value={selectedVal}
            />
            <ul>
              {props.items.map((item, i) => (
                /* ここで選択されたものだけスタイルを変えています */
                <li class={selectedVal == i ? "selected-item" : ""} onClick={event => select(i)}> {{ item }} </li>
              ))}
            </ul>
          </div>
        )}
      />
    </div>
  )
};

初見だと分かりづらいかもな、と思うのがこの Selected コンポーネントは自身のViewを持たないということです。render関数内でpropsとして渡されたrenderメソッドを呼び出しているだけで、自身はロジックしか持っていません。もはやこれを"コンポーネント"と呼ぶのが正しいのかすら微妙ですが、パターンとしてはそういう感じなので、今までのコンポーネントに対するメンタルモデルを少し変えてみましょう。

以上が実装例です。Selected コンポーネントのrender propをいじるだけで簡単に「選択された要素だけスタイルを変える」振る舞いを様々なコンポーネントに適用できるようになりました。

続いてScoped Slotを用いて同じ機能を実装してみたいと思います。

Scoped Slot

Slotとは何ぞやという話はドキュメントに譲るとして、Scoped Slotの何が普通のSlotと違うかと言いますと、その名の通り「Slotの配信先のコンポーネントのスコープのデータにアクセスすることができる」という点ですね。
こちらも実装して見たのでご覧ください!

Selected.vue
<template>
  <div>
    <slot name="selected" :selectedVal="selectedVal" :select="select">
      デフォルトの内容。
    </slot>
  </div>
</template>

<script>
export default {
  name: "Selected",
  data() {
    return { selectedVal: 0 };
  },
  methods: {
    select(value) {
      this.selectedVal = value;
    }
  }
}
</script>
App.vue
<template>
  <div id="app">
    <Selected>
      <ul
        slot="selected"
        slot-scope="{selectedVal, select}"> <!-- ここでSelectedコンポーネントのスコープにある変数・関数にアクセスしています -->
        <li
          v-for="(item, index) in props.items"
          :class="selectedVal == index ? 'selected-item' : ''"
          @click="select(index)"
        >
          {{ item }}
        </li>
      </ul>
    </Selected>
  </div>
</template>

<script>
  import Selected from './Selected';

  export default {
    name: 'hello',
    components:{
      Selected
    }
  }
</script>

Scoped Slotでも配信先のコンポーネントの変数・関数を扱えるため、それを用いて振る舞いだけを抽出することができることを確認しました。ラップされたコンポーネントのデータや関数にアクセスして振る舞いを追加するというのは、やってることはRender Propと同じだなと思います。

結論

実装例作ってみての結論ですが、どちらのパターンもEvan Youが言っている通り解いてる課題もできることも一緒だなと感じました。どっちが良いとかではなく書き方の違いでしかないので、描画関数を使っているか否かなど、携わっているVueプロジェクトのスタイルに応じて使い分ければ良いのではと思います。

それでは、本記事は以上となります。お読みいただきありがとうございました。

参考

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.