Getting Comfortable with Vue 3: setup, Reactivity, Lifecycle, and Everyday Patterns

Published:

Vue 3 changes the way components are written, but it does not force an all-or-nothing migration. You can still write code in the Vue 2 style, or start using the new APIs gradually in the same project. The biggest shift is that Vue 3 centers everything around functions, with setup acting as the main entry point for component logic.

What stands out in Vue 3

A few core changes define the Vue 3 development model:

  • Component logic is organized in a functional style rather than the earlier class-oriented approach.
  • The framework source is written in TypeScript, and TypeScript support in day-to-day development is stronger as a result.
  • Vue 3 remains compatible with Vue 2 patterns, so legacy-style code can still run alongside the new style.
  • If both styles are used in the same component, naming conflicts matter:
  • if a method returned from setup has the same name as one inside methods, Vue throws an error;
  • if a field returned from setup has the same name as a field from data, the data field takes precedence.

Another important change is the reactivity system. Vue 3 uses Proxy for data proxying. Instead of recursively walking through deeply nested data the way Vue 2 did, it proxies only the first layer up front, which helps improve rendering performance.

// 在vue3中定义一个响应式数据

const state = reactive({ data: { obj: {} } })

state.data.obj = xxx

In this example, state is a Proxy object. By default, the proxy initially wraps only the first level, such as data. Deep tracking still works because when you access state.data, its get handler runs and returns a proxied version of that nested object rather than the raw object itself. That means deeper properties are intercepted when they are actually accessed, instead of being recursively processed in advance.

setup: the center of the Composition API

In Vue 3, setup is where the new APIs come together. It runs only once, and it runs before lifecycle hooks.

That timing explains one of the first differences developers notice: inside setup, there is no current component instance available through this. So methods defined in the old Vue 2 style cannot be called with this from inside setup.

Vue 3 also removes the need to rely on data in the same way. Values exposed to the template come from what setup returns. If a returned value is just a constant, it will not become reactive automatically.

Event emission also changes form: this.$emit becomes context.emit.

// props - 组件接受到的属性, context - 上下文
setup(props, context){
    return {
        // 要绑定的数据和方法
    }
}

Lifecycle hooks are now callback-based

Lifecycle APIs in Vue 3 are used as functions inside setup. One practical difference is that the same lifecycle hook can be registered multiple times, and those callbacks run in the order they were registered.

setup() {
    onMounted(() => {
      console.log('组件挂载1');
    });

    onMounted(() => {
      console.log('组件挂载2');
    });

    onUnmounted(() => {
      console.log('组件卸载');
    });

    onUpdated(() => {
      console.log('组件更新');
    });

    onBeforeUpdate(() => {
      console.log('组件将要更新');
    });

    onActivated(() => {
      console.log('keepAlive 组件 激活');
    });

    onDeactivated(() => {
      console.log('keepAlive 组件 非激活');
    });

    return {};
  },

ref: reactivity for simple values

ref is used to wrap a plain value and make it reactive. It is mainly meant for simple values. Internally, the value is wrapped in an object and handled with defineProperty. When you read or update a ref in JavaScript, you do it through .value.

ref can also be used to get a component or element reference, replacing the old this.$refs pattern.

<template>
  <div class="mine">
    <input v-model="inputVal" />
    <button @click="addTodo">添加</button>
    <ul>
      <li v-for="(item, i) in todoList" :key="i">
        {{ item }}
      </li>
    </ul>
  </div>
  <div></div>
</template>


setup() {
    const inputVal = ref('');
    const todoList = ref<string[]>([]);

    function addTodo() {
      todoList.value.push(inputVal.value);
      inputVal.value = '';
    }

    return {
      addTodo,
      inputVal,
      todoList,
    };
  },

reactive: better suited for complex state

For objects and more complex data structures, reactive is the usual choice. It returns a Proxy object. When returning that state from setup, it is often convenient to use toRefs so the fields can be used directly in the template.

With reactive, you can place related state into a single object. Methods used by the template still need to be defined inside setup and returned explicitly.

Vue 3 templates also allow multiple sibling root elements, unlike Vue 2, where a template had to contain only one root element.

<template>
  <div class="mine">
    <input v-model="inputVal" />
    <button @click="addTodo">添加</button>
    <ul>
      <li v-for="(item, i) in todoList" :key="i">
        {{ item }}
      </li>
    </ul>
  </div>
  <div></div>
</template>

setup() {
    const data = reactive({
      inputVal: '',
      todoList: [],
    });

    function addTodo() {
      data.todoList.push(data.inputVal);
      data.inputVal = '';
    }

    return {
      ...toRefs(data),
      addTodo,
    };
  },

computed

Computed properties are now created through a function-based API. When the reactive dependencies change, the computed result is recalculated. In JavaScript, a computed value wrapped by computed is accessed with .value, though templates do not need .value.

async setup() {
    const data = reactive({
      a: 10,
      b: 20,
    });

    let sum = computed(() => data.a + data.b);

    return { sum };
  },

watch

Watching state also becomes function-based, but the idea stays close to Vue 2. You can watch a getter, or watch a ref directly.

// 侦听一个
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

watchEffect

watchEffect automatically tracks any reactive values used inside its function. When one of those values changes, the function runs again.

const count = ref(0)
// 当count的值被修改时,会执行回调
watchEffect(() => console.log(count.value))

Using the router inside components

Vue Router access also moves away from this. In Vue 3 components, routing is typically handled through useRoute and useRouter.

import { useRoute, useRouter } from 'vue-router'

const route = useRoute() // 相当于 vue2 中的this.$route
const router = useRouter() // 相当于 vue2 中的this.$router

// route   用于获取当前路由数据
// router  可以的得到一些关于路由的方法

route gives you the current route information, while router exposes navigation-related methods.

Accessing Vuex in Vue 3

Vuex is accessed through useStore. One detail is especially important: when reading values from the store for use in the template, wrap them with computed, otherwise changes in Vuex state will not update the page reactively.

import {useStore} from 'vuex'

setup(){
    const store = useStore(); // 相当于 vue2中的 this.$store
    store.dispatch(); // 通过store对象来dispatch 派发异步任务
    store.commit(); // commit 修改store数据

    let category = computed(() => store.state.home.currentCagegory
    return { category }
}

Defining Vue components with JSX

Vue 3 can also define components with JSX syntax, which gives another way to write render logic directly in JavaScript.

export const AppMenus = defineComponent({
  setup() {
    return () => {
      return (
        <div class="app-menus">
          <h1>这是一个vue组件</h1>
        </div>
      )
    }
  },
})

Slot syntax

The slot model remains familiar, including named slots and the default slot:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

For developers moving from Vue 2, Vue 3 mainly asks for a shift in how component logic is organized. Once setup, ref, reactive, and the function-based lifecycle APIs become familiar, the rest of the framework starts to feel consistent.