🙌

vueコンポーネントをcomposablesから生成する

2023/08/04に公開

vueコンポーネントをcomposablesから生成する

始めに

先日vueの勉強会で、ロジックとUIを切り離す為コンポーネントをcomposabelsから生成する方法を知ったので、簡単なデモを動かしまとめました。

よく使うような簡単なコンポーネントをbefore,afterで書き換えてみました。
まずは結果から。

before

main.vue(before)
<template>
  <div>
    // props,emitを持つコンポーネント
    <VChild1 :count="count" @doSomething="doSomething" />
    
    // props,内部関数,clickイベントを持つコンポーネント
    <VChild2 ref="VChild2Ref" :count="count" @click="doSomething" />
    
    // slotを持つコンポーネント
    <VChild3>
      <template #default>
        <div>default slot</div>
      </template>
      <template #footer>
        <ul>footer slot</ul>
      </template>
    </VChild3>
    
    // 状態によって表示表示を切り替えるコンポーネント
    <VChild4 v-if="isShow" />

    <LButton @click="count++">カウントアップ</LButton>
    <LButton @click="VChild2Ref?.doSomething()">VChild2の内部関数実行</LButton>
    <LButton @click="isShow = !isShow">VChild4の表示非表示</LButton>
  </div>
</template>

<script lang="ts" setup>
import VChild1 from '~/features/dev/components/VChild1.vue';
import VChild2 from '~/features/dev/components/VChild2.vue';
import VChild3 from '~/features/dev/components/VChild3.vue';
import VChild4 from '~/features/dev/components/VChild4.vue';
imoprt useChild from '../composables/useChild';

const {count,doSomething,isShow} = useChild();

const VChild2Ref = ref<InstanceType<typeof VChild2>>();

</script>
useChild.ts(after)
import _VChild1 from '../components/VChild1.vue';
import _VChild2 from '../components/VChild2.vue';
import _VChild3 from '../components/VChild3.vue';
import _VChild4 from '../components/VChild4.vue';

export const useChild = () => {
  const count = ref<number>(0);
  const isShow = ref<boolean>(false);
  const doSomething = () => {
    alert('doSomething')
  }
  return {
    count,
    doSomething,
    isShow
  };
};

after

main.vue(after)
<template>
  <div>
    <VChild1 />
    <VChild2 />
    <VChild3 />
    <VChild4 />

    <LButton @click="countUp">カウントアップ</LButton>
    <LButton @click="VChild2Ref?.doSomething()">VChild2の内部関数実行</LButton>
    <LButton @click="toggleShow">VChild4の表示非表示</LButton>
  </div>
</template>

<script lang="ts" setup>
import { useChild } from '@/features/dev/composables/useChild';
const { VChild1, VChild2, VChild3, VChild4, countUp, VChild2Ref, toggleShow } =
  useChild();
</script>
useChild.ts(after)
import _VChild1 from '../components/VChild1.vue';
import _VChild2 from '../components/VChild2.vue';
import _VChild3 from '../components/VChild3.vue';
import _VChild4 from '../components/VChild4.vue';

export const useChild = () => {
  const count = ref<number>(0);

  // VChild1(propsとemit)
  const VChild1Render = () =>
    h(_VChild1, {
      count: count.value,
      onNotify: (msg: number) => {
        alert(msg);
      },
    });

  const VChild1 = defineComponent({
    render: VChild1Render,
  });

  // VChild2(propsとrefのバインド)
  const VChild2Ref = ref<InstanceType<typeof _VChild2>>();
  const VChild2Render = () =>
    h(_VChild2, {
      count: count.value,
      ref: VChild2Ref,
    });

  const VChild2 = defineComponent({
    render: VChild2Render,
  });

  // VChild3(slot)
  const VChild3Render = () =>
    h(
      _VChild3,
      null,
      {
        default: () => 'default slot',
        fotter: () => h('ul', 'footer'),
      },,
    );

  const VChild3 = defineComponent({
    render: VChild3Render,
  });

  // VChild4(コンポーネントの表示非表示)
  const isShow = ref(true);
  const VChild4Render = () => {
    if (isShow.value) {
      const _h = h(_VChild4, {
        count: count.value,
      });
      return _h;
    }

    const _h = h(() => null);
    return _h;
  };

  const VChild4 = defineComponent({
    render: VChild4Render,
  });

  const countUp = () => {
    count.value++;
  };

  const toggleShow = () => {
    isShow.value = !isShow.value;
  };
  return {
    VChild1,
    VChild2,
    VChild3,
    VChild4,
    countUp,
    VChild2Ref,
    toggleShow,
  };
};

VChild1(propsとemit)

useChild.ts
  const count = ref<number>(0);
  const VChild1Render = () =>
    h(_VChild1, {
      count: count.value,
      onNotify: (msg: number) => {
        alert(msg);
      },
    });

  const VChild1 = defineComponent({
    render: VChild1Render,
  });

props,emitをh()の第二引数に指定。
イベントリスナーはon+イベント名で指定できる。https://ja.vuejs.org/api/render-function.html#h

VChild2(refのバインド)

useChild.ts
  const VChild2Ref = ref<InstanceType<typeof _VChild2>>();
  const VChild2Render = () =>
    h(_VChild2, {
      count: count.value,
      ref: VChild2Ref,
    });

  const VChild2 = defineComponent({
    render: VChild2Render,
  });

propsと同様、refを第二引数で指定してバインドする。

VChild3(slot)

useChild.ts
  const VChild3Render = () =>
    h(
      _VChild3,
      null,
      {
        default: () => 'default slot',
        fotter: () => h('ul', 'footer'),
      },
    );

  const VChild3 = defineComponent({
    render: VChild3Render,
  });

第三引数でslot名を指定してslotを渡すことが可能。

VChild4(コンポーネントの表示非表示)

useChild.ts
  const isShow = ref(true);
  const VChild4Render = () => {
    if (isShow.value) {
      const _h = h(_VChild4, {
        count: count.value,
      });
      return _h;
    }

    const _h = h(() => null);
    return _h;
  };

  const VChild4 = defineComponent({
    render: VChild4Render,
  });

render関数の中で条件分岐することで、refの値に応じて生成する要素を分岐。

メリット

  • UIファイルがかなりシンプルになり、ロジックがcomposablesファイルでほぼ完結する

デメリット

  • UIファイルがシンプルすぎて、ロジックファイルを読まないと表示条件やイベントが把握できない
  • 実装コストは上がりそう、型エラーが起きた時に分かりにくい

終わりに

viewファイルからロジックを切り離すという意味では魅力的でした。
ただ、実装コストや可読性含めて良い使い所を見つけていく必要があるなと思いました。

Discussion