enpitsulin

enpitsulin

这个人很懒,没有留下什么🤡
twitter
github
bilibili
nintendo switch
mastodon

Preview and Simple Evaluation of New Features in Vue 3.3

Although 3.3 is currently in beta, some of its features are very exciting. Here’s a brief preview of the new features to prepare for future upgrades. 😁

Generic Component Support#

Vue has always struggled to implement generic components effectively, but finally, this functionality has been added in version 3.3.

First, for TSX users, the defineComponent utility function has been enhanced with generic support. When a generic function is passed as a parameter, the type hints work correctly. For example, we can use this feature to easily construct a table component in TSX.

import { defineComponent } from 'vue';

type Props<T> = { data: T[]; columns: { label: string; field: keyof T }[] };

const Table = defineComponent(<T,>(props: Props<T>) => {
  return () => (
    <table>
      <thead>
        <tr>
          {props.columns?.map((item) => (
            <th>{item.label}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {props.data?.map((row) =>
          props.columns?.map((column) => <td>{row[column.field]}</td>)
        )}
      </tbody>
    </table>
  );
});

export default Object.assign(Table, {
  name: 'GenericsTableTsx',
  props: ['columns', 'data']
});

However, it is worth noting that we still need to pass props attributes to this component; otherwise, attributes that should be props will be mounted to $attrs, which essentially discourages such usage. Therefore, while the type correctness is there, it is not highly recommended to build generic components this way for production.

SFC Generic Component Support#

The functionality above is actually a precursor to this. Let's see how to replicate the above component using SFC.

<template>
  <table>
    <thead>
      <tr>
        <th v-for="item in columns">
          <slot name="header-cell" v-bind="{ label: item.label }">
            {{ item.label }}
          </slot>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in data">
        <td v-for="column in columns">
          <slot name="cell" v-bind="{ data: row[column.field] }">
            {{ row[column.field] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>
<script setup lang="ts" generic="T">
  const { columns, data } = defineProps<{
    data: T[];
    columns: {
      label: string;
      field: keyof T;
    }[];
  }>();
</script>

[email protected] adds a generic attribute to script for creating generic parameters, and multiple parameters can be separated by , just like in TS.

Comment: A powerful new feature; it's truly commendable that Vue finally has generic components. However, the requirement to additionally pass props for TSX support is somewhat cumbersome. This issue has been around for a while, and I hope the Vue team will work on improving the development experience for TSX in the future.

Type Support for defineProps Macro#

This request has been around for two years, but most developers have been using some community plugins to achieve this functionality. Now, the official support has finally arrived, and in 3.3, we can easily create Props using externally imported types.

<script setup lang="ts">
import type { SomeType } from 'some-where'

const props = defineProps<{ data: SomeType }>()
</script>

Comment: As expected, this makes it easier to manage types in Vue projects without having to write lengthy type gymnastics in SFCs.

More Convenient Syntax for defineEmits Macro#

For 3.2, defineEmits needed to be used like this based on generics:

defineEmits<{
  (e: 'foo', id: string): void
  (e: 'bar',...args: any[]): void
}>()

In 3.3, the syntax allows defining a single parameter using named tuples. If using rest params, the parameter can be directly defined as T[].

defineEmits<{
  foo: [id: string]
  bar: any[]
}>()

Comment: A small feature that enhances developer experience; writing too many emits in function overload form can indeed be a bit tedious.

New Tools for v-model#

This comes from 智子君 as a new feature, allowing the use of defineModel in <script setup/> and useModel in non-<script setup/>.

// Default model (via `v-model`)
const modelValue = defineModel()
   // ^? Ref<any>
modelValue.value = 10

const modelValue = defineModel<string>() // Add type
   // ^? Ref<string | undefined>
modelValue.value = "hello"

// Default model with settings, requires non-undefined 
const modelValue = defineModel<string>({ required: true })
   // ^? Ref<string>

// Specific name model (via `v-model:count` )
const count = defineModel<number>('count')
count.value++

// Specific name model with default value
const count = defineModel<number>('count', { default: 0 })
   // ^? Ref<number>

// Locally mutable model, as the name suggests
// Can avoid needing to pass v-model from the parent component
const count = defineModel<number>('count', { local: true, default: 0 })

There’s also useModel as a tool for use in non-<script setup/>.

import { useModel } from 'vue'

export default {
  props: {
    modelValue: { type: Number, default: 0 },
  },
  emits: ['update:modelValue'],
  setup(props) {
    const modelValue = useModel(props, 'modelValue')
       // ^? Ref<number>

    return { modelValue }
  }
}

Comment: Another tool that enhances developer experience; defining a v-model property can indeed be cumbersome, and its practicality within SFCs is limited. It generally needs to be used in conjunction with vueuse/useVModels. The official addition of this macro and utility function is quite nice.

defineOptions#

This is another PR from 智子君,previously from an RFC. Many people have likely used this in Vue Macro.

Previously, if you needed to define some original Option API properties like inheritAttrs/name in <script setup>, you had to create a separate <script> to export these two properties. Now, with defineOptions, you can skip this step.

<script setup>
// Some code
</script>
<script>
export default {
  name: "ComponentName"
}
</script> 
<script setup>
defineOptions({
  name: "ComponentName"
})
// Some code
</script> 

Comment: This feature can be used in Vue Macro for an early experience. I find it very enjoyable to use.

defineSlots Macro and slots Property#

Also from 智子君,TQL.

Allows defining specific types for slots. First, a new SlotsType has been added, and the slots property can be used in the options API.

import { SlotsType } from 'vue'

export default defineComponent({
  slots: Object as SlotsType<{
    default: { foo: string; bar: number }
    item: { data: number }
  }>,
  setup(props, { slots }) {
    expectType<undefined | ((scope: { foo: string; bar: number }) => any)>(
      slots.default
    )
    expectType<undefined | ((scope: { data: number }) => any)>(slots.item)
  }
})

For this defined component SlotComponent, using it in a component would look like this:

<template>
  <SlotComponent>
    <template #default="{ foo, bar }">
      {{ foo }} is string,{{ bar }} is number
    </template>
    <template #item="{ data }">
      {{ data }} is number
    </template>
  </SlotComponent>
</template>

defineSlots is similar to the slots property but provides a function syntax.

// Consistent with object syntax, thanks to ES for not treating default as a property keyword. How delightful! 😆
const slots = defineSlots<{
  default(props: { foo: string; bar: number }): any // or VNode[]
}>()

Comment: Having correct types for slots is great news for component libraries, as users have struggled to determine how to use a particular scopeSlot since the introduction of scopeSlot.

Using console.log in Templates#

Sudden debugging may require console.log, but it was difficult to use in templates. Now, 3.3 adds extra support, so you no longer need to create a function in the template scope to print things.

Comment: This is a minor enhancement that improves developer experience; occasionally using it feels quite comfortable.

Less Important Features#

Improvements to Suspense#

Personally, I think Vue's <Suspense> can be temporarily ignored; it's been an experimental feature for a long time. The PR is here.

Deprecated and Modified Features#

v-is Directive Deprecated#

The v-is directive has been deprecated, and all should use the :is directive instead (I wonder if anyone is still using this directive?).

app.config.unwrapInjectedRef#

The app.config.unwrapInjectedRef property is gone. In 3.3, it will default to unwrapping refs injected using the Option API.

vnode hook Deprecated#

The vnode hook seems to be used by fewer users; a @vnode-* directive has changed to @vue:*. This feature is likely used infrequently, and there isn't much information about it online. It seems to provide functionality similar to component lifecycle methods for Vnodes. If anyone knows what this feature can do, please share.

Improvements for Ecosystem Developers#

app.runWithContext()#

A runWithContext() method has been added to the App, which can be used to ensure the existence of global variables at the application level, such as values provided through provide. This can improve various packages in the Vue ecosystem, like pinia/vue-router.

const app = createApp(App)

app.provide('foo', 1)

app.runWithContext(() => inject('foo')) // should return 1

hasInjectionContext#

hasInjectionContext is a tool for library authors based on Vue to check if inject() can be used. If the current environment allows it, it returns true; otherwise, it indicates that the environment is outside of setup. Library authors can use this function to avoid additional checks for the current environment.

Comment: These features are quite useful for ecosystem developers. If you are writing a library, take note.

Important Note#

For TSX users, Vue 3.3 no longer automatically registers the global JSX namespace. You need to manually modify jsxImportSource in tsconfig.json or use the magic comment /* @jsxImportSource vue */ to avoid global JSX type conflicts.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.