Skip to content

Vue-class-component

ES / TypeScript decorator for class-style Vue components.

Deprecated ??

2년째 갱신되지 않고 있기에(2022-04-20 기준) 지원 중단에 대한 이야기가 나오고 있다. 그리고 vue3의 Composition API와 함께 사용되는 것(<code>

<script setup>

...

</script>

</code>)에 대한 타당성(?) ... 같은 것이 논의되고 있다.

Build Setup

TypeScript

Create tsconfig.json on your project root and specify experimentalDecorators option so that it transpiles decorator syntax:

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "strict": true,
    "experimentalDecorators": true
  }
}

Class Component

@Component decorator makes your class a Vue component:

import Vue from 'vue'
import Component from 'vue-class-component'

// HelloWorld class will be a Vue component
@Component
export default class HelloWorld extends Vue {}

Data

Initial data can be declared as class properties:

<template>
  <div>{{ message }}</div>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // Declared as component data
  message = 'Hello World!'
}
</script>

The above component renders Hello World! in the <div> element as message is component data.

Note that if the initial value is undefined, the class property will not be reactive which means the changes for the properties will not be detected:

import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // `message` will not be reactive value
  message = undefined
}

To avoid this, you can use null value or use data hook instead:

import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // `message` will be reactive with `null` value
  message = null

  // See Hooks section for details about `data` hook inside class.
  data() {
    return {
      // `hello` will be reactive as it is declared via `data` hook.
      hello: undefined
    }
  }
}

Methods

Components methods can be declared directly as class prototype methods:

<template>
  <button v-on:click="hello">Click</button>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // Declared as component method
  hello() {
    console.log('Hello World!')
  }
}
</script>

Computed Properties

Computed properties can be declared as class property getter / setter:

<template>
  <input v-model="name">
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  firstName = 'John'
  lastName = 'Doe'

  // Declared as computed property getter
  get name() {
    return this.firstName + ' ' + this.lastName
  }

  // Declared as computed property setter
  set name(value) {
    const splitted = value.split(' ')
    this.firstName = splitted[0]
    this.lastName = splitted[1] || ''
  }
}
</script>

Hooks

data, render and all Vue lifecycle hooks can be directly declared as class prototype methods as well, but you cannot invoke them on the instance itself. When declaring custom methods, you should avoid these reserved names.

import Vue from 'vue'
import Component from 'vue-class-component'

@Component
export default class HelloWorld extends Vue {
  // Declare mounted lifecycle hook
  mounted() {
    console.log('mounted')
  }

  // Declare render function
  render() {
    return <div>Hello World!</div>
  }
}

Other Options

For all other options, pass them to the decorator function:

<template>
  <OtherComponent />
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'
import OtherComponent from './OtherComponent.vue'

@Component({
  // Specify `components` option.
  // See Vue.js docs for all available options:
  // https://vuejs.org/v2/api/#Options-Data
  components: {
    OtherComponent
  }
})
export default class HelloWorld extends Vue {}
</script>

TypeScript Example

import Vue from 'vue'
import Component from 'vue-class-component'

// @Component 데코레이터는 클래스가 Vue 컴포넌트임을 나타냅니다.
@Component({
  // 모든 컴포넌트 옵션이 이곳에 허용됩니다.
  template: '<button @click="onClick">Click!</button>'
})
export default class MyComponent extends Vue {
  // 초기 데이터는 인스턴스 속성으로 선언할 수 있습니다.
  message: string = 'Hello!'

  // 컴포넌트 메소드는 인스턴스 메소드로 선언할 수 있습니다.
  onClick (): void {
    window.alert(this.message)
  }
}

Troubleshooting

속성 변경시 갱신되지 않는 현상

import {Vue, Component} from 'vue-property-decorator';

@Component
export default class  extends Vue {
  _paused = false;
  get status() {
    return this._paused ? "A" : "B";
  }
}

위와 같은 코드가 있을 때, _로 시작하는 속성은 get status() 에 반영되지 않더라... _을 제거하면 된다.

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders

Vue.js#Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders 항목 참조.

Prop 으로 지정하지 않은, Data member 인데 이 경고가 출력될 경우

컴포넌트를 포함시킬 때, 하위 컴포넌트에서 속성이 중복되면 저런 에러가 출력된다.

<script lang="ts">
import {Component, Prop, Emit} from 'vue-property-decorator';
import VueBase from '@/base/VueBase';

@Component
export default class TableProjects extends VueBase {

  // ...

  loading = false;

  // ...
}
</script>

에러:

vue.esm.js?a026:628 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "loading"

found in

---> <TableProjects>
       <MainAdminProjects> at src/pages/MainAdminProjects.vue
         <MainAdmin> at src/pages/MainAdmin.vue
           <VMain>
             <VApp>
               <Main> at src/pages/Main.vue
                 <VApp>
                   <App> at src/App.vue
                     <Root>

vue-devtools를 설치하여 확인해보니, loadingdata 가 아닌, props로 등록되어 있었다.

디버깅/추적은 다음과 같이 진행하였다. 위의 에러가 출력되는 console.error 위치에 Breakpoint 를 찍고, 원인이 되는 변수의 할당 위치를 조금씩 추적한다.

결국, vue-class-component의 componentFactory 함수에 도달, 해당 함수에서 다음 위치가 문제로 확인되었다.

  // ...

  var decorators = Component.__decorators__;

  if (decorators) {
    decorators.forEach(function (fn) {
      return fn(options);
    });
    delete Component.__decorators__;
  } // find super

  // ...

위의, Component.__decorators__에, 다른 컴포넌트에서 선언한 데코레이터들이 모두 (e.g. @Prop) 모두 모여져 있었고, 다른 컴포넌트에서 @Prop() readonly loading!: boolean;으로 선언한 데코레이터 선언 함수가 호출되었다.

위의, ComponentComponent.__decorators__를 선언하는 위치는, vue-class-componentcreateDecorator 함수였고, 구현은 다음과 같다:

export function createDecorator (factory: (options: ComponentOptions<Vue>, key: string, index: number) => void): VueDecorator {
  return (target: Vue | typeof Vue, key?: any, index?: any) => {
    const Ctor = typeof target === 'function'
      ? target as DecoratedClass
      : target.constructor as DecoratedClass
    if (!Ctor.__decorators__) {
      Ctor.__decorators__ = []
    }
    if (typeof index !== 'number') {
      index = undefined
    }
    Ctor.__decorators__.push(options => factory(options, key, index))
  }
}

위의 내용으로 미루어 볼 때, Ctor 변수가 모든 컴포넌트에 공유되는 현상이 문제가 되었고, 3~5번째 줄의 targetVueComponent 인스턴스 여야 하는데(확인 필요) 클래스의 프로토타입 이였다.

위 내용으로 미루어 보아, 처음 클래스 정의부 에서 VueBase 클래스가 컴포넌트가 아니여서 발생된 문제로 확인되었다.

console.dir(VueBase) 출력이 ƒ VueComponent(options)로 나와야 하지만, 출력해보니 ƒ VueBase()로 출력되었다. (참고로 console.dir(Vue)는 출력이 ƒ Vue(options) 이다.)

최종적으로 아래의 코드를

export default class VueBase extends Vue {
  // ...
}

아래와 같이 바꿨다.

@Component
export default class VueBase extends Vue {
  // ...
}

그리고 해결.

TypeScript Getter 오동작 오류

다음과 같이 vue-router 를 사용하는 코드가 있다.

@Component
export default class VueBase extends Vue {
    get currentRouteName(): string {
        return this.$router.currentRoute.name || '';
    }

    moveTo(name: string, params?: object) {
        if (this.currentRouteName === name) {
            return;
        }

        const rawLocation = {
            name: name,
            params: params,
        } as RawLocation;

        this.$router.push(rawLocation).catch((reason: any) => {
            if (reason.name !== 'NavigationDuplicated') {
                throw reason;
            }
        })
    }
}

위의 currentRouteName 함수는 Getter 로 지정되어 있는데, @Component를 통해 Computed 함수로 변환된다.

Computed Getter 의 경우 구현체는 아래와 같다:

function createComputedGetter (key) {
  return function computedGetter () {
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value
    }
  }
}

/**
 * Evaluate the value of the watcher.
 * This only gets called for lazy watchers.
 */
Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();
  this.dirty = false;
};

/**
 * Depend on all deps collected by this watcher.
 */
Watcher.prototype.depend = function depend () {
  var i = this.deps.length;
  while (i--) {
    this.deps[i].depend();
  }
};

코드를 분석하면 알겠지만, watcher.value 값으로 한번 캐싱되고, 종속 변수의 수정 또는 watcher 를 통한 값의 변경(dirty)이 없으면 캐싱된 내용으로 반환된다.

문제는, this.$router.currentRoute.name로 지정된 변수는 위의 두 가지 조건에 모두 부합되지 않는다. 이를 해결하려면 this.$router.currentRoute.name@Watch 로 확인하거나, get을 사용한 computed 적용을 피해야 한다.

See also

Favorite site