enpitsulin

enpitsulin

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

Vue 3.3 New Features Preview and Simple Evaluation

Although 3.3 is still 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 added this functionality 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, the attributes that should be props will be mounted on $attrs. This essentially discourages such usage, so while the type is correct, it is not recommended to build generic components this way for production.

SFC Generic Component Support#

The above functionality is actually a precursor to this; we will 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 need to additionally pass props for TSX support is somewhat cumbersome, and this issue has been around for a while. 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 is finally here; in 3.3, we can easily use externally imported types to create Props.

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

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

Comment: As expected, it makes managing types in Vue projects more convenient, eliminating the need 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 for a single parameter uses named tuples, and for rest params, you can directly use T[].

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

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

New Tools for v-model#

This comes from 智子君 with a new feature that allows 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 required setting, must be 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 be used without passing 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 the developer experience; defining a v-model property can be quite 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 indeed a great improvement.

defineOptions#

This is another PR from 智子君,originally from 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 the specific types of 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; thankfully, ES did not treat 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 might 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: Not a big deal, but it enhances the developer experience; it feels nice to use occasionally.

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; all should now use the :is directive (I wonder if anyone is still using this directive?).

app.config.unwrapInjectedRef#

The app.config.unwrapInjectedRef property is gone. In 3.3, it will automatically unwrap refs injected using the Option API.

vnode hook Deprecated#

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

Improvements for Ecosystem Developers#

app.runWithContext()#

A new runWithContext() method has been added to the App 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, pay attention to this.

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.