Things related to dependency injection and component definition in Vue 3

Let’s talk Vue 3about dependency injection and component definition.

main content

In this sharing, we mainly cover the following contents:

  • ūüď̬†provide() & inject()¬†– dependency injection
  • ūüõ†¬†nextTick()¬†– after the next DOM update cycle
  • ūüé®Component¬†definition
    • defineComponent()¬†– component definition type inference helper function
    • defineAsyncComponent()¬†– asynchronous component
    • defineCustomElement()¬†– Constructor for native custom element classes

provide() & inject()

provide()

Provides a value that can be injected by descendant components.

function provide<T>( key : InjectionKey <T> | string , value : T): void

Receives two parameters:

  • To be injected¬†key, string or¬†Symbol;
export  interface  InjectionKey <T> extends  Symbol {}
  • Corresponding to the value injected

Similar to registering life cycle hooks APIprovide()they must be called synchronously in the component’s setup()phase.

inject()

app.provide()Inject a value provided by an ancestor component or the entire application (via ).

// No default value 
function inject<T>( key : InjectionKey <T> | string ): T | undefined

//With default value 
function inject<T>( key : InjectionKey <T> | string , defaultValue : T): T

// Use factory function 
function inject<T>(
   key : InjectionKey <T> | string ,
   defaultValue : () => T,
   treatDefaultAsFactory : true 
): T
  • The first parameter is injected¬†key.¬†VueThe parent component chain is traversed to¬†keydetermine the provided value by matching . If multiple components in the parent component chain provide a value for the same¬†key, then the closer component will “overwrite” the value provided by the component further up the chain. If no¬†keyvalue is matched by ,¬†inject()it will be returned¬†undefinedunless a default value is provided.
  • The second parameter is optional and is¬†keythe default value used if no match is found. It can also be a factory function that returns some value that is more complex to create. If the default value itself is a function, then you must¬†falsepass it in as the third parameter, indicating that this function is the default value, not a factory function.

provide() & inject() – official example

//provide
<script setup>
  import {(ref, provide)} from  'vue'  import {fooSymbol} from 
  './injectionSymbols'  // Provide static values ‚Äč‚Äčprovide('foo', 'bar') // Provide responsive values 
  ‚Äč‚Äčconst count = ref ( 0 ) provide ( 'count' , count) // Use Symbol as key when providing 
  provide (fooSymbol, count)
</script>
//inject
<script setup>
import { inject } from  'vue' 
import { fooSymbol } from  './injectionSymbols'

//The default way to inject values 
‚Äč‚Äčis const foo = inject ( 'foo' )

//Inject the response value 
const count = inject ( 'count' )


// Inject const foo2 = inject (fooSymbol) through the key of Symbol type

// Inject a value, if empty, use the provided default value 
const bar = inject ( 'foo' , 'default value' )

// Inject a value, if it is empty, use the provided factory function 
const baz = inject ( 'foo' , () =>  new  Map ())

// When injecting, in order to indicate that the default value provided is a function, the third parameter 
const fn = inject ( 'function' , () => {}, false ) needs to be passed in
</script>

provide() & inject() – ElementUI Plus sample Breadcrumb component

<script lang= "ts" setup>
 import { onMounted, provide, ref } from  'vue' 
import { useNamespace } from  '@element-plus/hooks' 
import { breadcrumbKey } from  './constants' 
import { breadcrumbProps } from  ' ./breadcrumb'

defineOptions ({
   name : 'ElBreadcrumb' ,
})

const props = defineProps (breadcrumbProps)
 const ns = useNamespace ( 'breadcrumb' )
 const breadcrumb = ref< HTMLDivElement >()
 // Provide value 
provide (breadcrumbKey, props)

onMounted ( () => {
  ...
})
</script>
<script lang= "ts" setup>
 import { getCurrentInstance, inject, ref, toRefs } from  'vue' 
import  ElIcon  from  '@element-plus/components/icon' 
import { useNamespace } from  '@element-plus/hooks' 
import { breadcrumbKey } from  './constants' 
import { breadcrumbItemProps } from  './breadcrumb-item'

import  type { Router } from  'vue-router'

defineOptions ({
   name : 'ElBreadcrumbItem' ,
})

const props = defineProps (breadcrumbItemProps)

const instance = getCurrentInstance ()!
 // Inject value 
const breadcrumbContext = inject (breadcrumbKey, undefined )!
 const ns = useNamespace ( 'breadcrumb' )
 ...
</script>

provide() & inject() – VueUse example

createInjectionState source code / createInjectionState usage

package/core/computedInject source code

import { type  InjectionKey , inject, provide } from  'vue-demi'

/**
 * Create global state that can be injected into components
 */ 
export  function createInjectionState< Arguments  extends  Array < any >, Return >(
   composable : ( ...args: Arguments ) =>  Return 
): readonly [
   useProvidingState : ( ...args: Arguments ) =>  Return ,
   useInjectedState : ( ) =>  Return | undefined
] {
  const  key : string | InjectionKey < Return > = Symbol ( 'InjectionState' )
   const  useProvidingState = ( ...args: Arguments ) => {
     const state = composable (...args)
     provide (key, state)
     return state
  }
  const  useInjectedState = () => inject (key)
   return [useProvidingState, useInjectedState]
}

nextTick()

Utility method that waits for the next DOM update to be refreshed.

function  nextTick ( callback?: () => void ): Promise < void >

Note: When you Vuechange reactive state in , the final DOMupdates are not synchronized, but are Vuecached in a queue until the next one ‚Äútick‚ÄĚis executed together. This is to ensure that each component only performs one update regardless of how many state changes occur.

nextTick()Can be used immediately after a status change to wait for DOMthe update to complete. You can pass a callback function as a parameter, or await the Promise returned .

nextTick() official website example

<script setup>
 import { ref, nextTick } from  'vue'

const count = ref ( 0 )

async  function  increment () {
  count.value ++‚Äč

  // DOM has not been updated 
  console . log ( document . getElementById ( 'counter' ) . textContent ) // 0

  await  nextTick ()
   // DOM has been updated at this time 
  console . log ( document . getElementById ( 'counter' ). textContent ) // 1
}
</script>

< template > 
  < button  id = "counter" @ click = "increment" > {{ count }} </ button > 
</ template >

nextTick() – ElementUI Plus example

ElCascaderPanel source code

export  default  defineComponent ({
  ...
  const  syncMenuState = (
    newCheckedNodes: CascaderNode[],
    reserveExpandingState = true
   ) => {
    ...
    checkedNodes. value = newNodes
     nextTick (scrollToExpandingNode)
  }
  const  scrollToExpandingNode = () => {
     if (!isClient) return 
    menuList. value . forEach ( ( menu ) => {
       const menuElement = menu?. $el 
      if (menuElement) {
         const container = menuElement. querySelector ( `. ${ns. namespace . value} -scrollbar__wrap` )
         const activeNode = menuElement. querySelector ( `. ${ns.b( 'node' )} . ${ns.is( 'active' )} ` ) ||
          menuElement. querySelector ( `. ${ns.b( 'node' )} .in-active-path` )
         scrollIntoView (container, activeNode)
      }
    })
  }
  ...
})

nextTick() – VueUse example

useInfiniteScroll source code

export  function  useInfiniteScroll ( 
  element: MaybeComputedRef<HTMLElement | SVGElement | Window | Document | null | undefined >
  ...
) {
   const state = reactive (......)
   watch (
     () => state. arrivedState [direction],
     async (v) => {
       if (v) {
         const elem = resolveUnref (element) as  Element
        ...
        if (options. preserveScrollPosition && elem) {
           nextTick ( () => {
            elem. scrollTo ({
               top : elem. scrollHeight - previous. height ,
               left : elem. scrollWidth - previous. width ,
            })
          })
        }
      }
    }
  )
}

scenes to be used:

  1. DOMWhen you need to operate immediately after modifying some data , you can use nextTickto ensure that DOMhas been updated. For example, when using $refto obtain an element, you need to ensure that the element has been rendered before it can be obtained correctly.
  2. In some complex pages, some components may change frequently due to conditional rendering or dynamic data. Using can improve application performance by avoiding nextTickfrequent operations.DOM
  3. When you need to access certain calculated properties or values ‚Äč‚Äčin the listener in the template, you can also use¬†nextTickto ensure that these values ‚Äč‚Äčhave been updated. This avoids accessing old values ‚Äč‚Äčin the view.

In short, nextTickit is a very useful API that can ensure that DOMoperations are performed at the right time, avoid unnecessary problems, and improve application performance.

defineComponent()

VueProvides helper functions for type inference when defining components.

function  defineComponent ( 
  component: ComponentOptions | ComponentOptions[ 'setup' ]
 ): ComponentConstructor

The first parameter is a component options object. The return value will be the options object itself, since the function actually does nothing at runtime and is only used to provide type deduction.

Note that the type of the return value is a little special: it will be a constructor type, and its instance type is the component instance type inferred based on the options. This is TSXto provide type inference support when the return value is used as a label in .

const  Foo = defineComponent ( /* ... */ )
 // Extract the instance type of a component (equivalent to the type of this in its options) 
type  FooInstance = InstanceType < typeof  Foo >

Reference: Vue3 – What does defineComponent solve?

defineComponent() – ElementUI Plus example

ConfigProvider source code

import { defineComponent, renderSlot, watch } from  'vue' 
import { provideGlobalConfig } from  './hooks/use-global-config' 
import { configProviderProps } from  './config-provider-props'
...
const  ConfigProvider = defineComponent ({
   name : 'ElConfigProvider' ,
   props : configProviderProps,

  setup ( props, { slots } ) {
    ...
  },
})
export  type  ConfigProviderInstance = InstanceType < typeof  ConfigProvider >

export  default  ConfigProvider

defineComponent() – Treeshaking

Because defineComponent()is a function call, it may be considered to have side effects by some build tools, webpacke.g. Even if a component is never used, it may not be tree-shake.

To tell that webpackthis function call can be made safely tree-shake, we can add a comment of the form before the function call /_#**PURE**_/:

export  default  /*#__PURE__*/  defineComponent ( /* ... */ )

Please note that if you are using it in your project Vite, you don’t need to do this, because RollupVitethe underlying production environment packaging tool used) can intelligently determine that defineComponent()there are actually no side effects, so there is no need to manually annotate it.

defineComponent() – VueUse example

OnClickOutside source code

import { defineComponent, h, ref } from  'vue-demi' 
import { onClickOutside } from  '@vueuse/core' 
import  type { RenderableComponent } from  '../types' 
import  type { OnClickOutsideOptions } from  '.' 
export  interface  OnClickOutsideProps  extends  RenderableComponent {
  options?: OnClickOutsideOptions
}
export  const  OnClickOutside = /* #__PURE__ */ defineComponent< OnClickOutsideProps >({
     name : 'OnClickOutside' ,
     props : [ 'as' , 'options' ] as  unknown  as  undefined ,
     emits : [ 'trigger' ],
     setup ( props, { slots, emit } ) {
      ... ...

      return  () => {
         if (slots. default )
           return  h (props. as || 'div' , { ref : target }, slots. default ())
      }
    },
  })

defineAsyncComponent()

Define an asynchronous component that is loaded lazily at runtime. The argument can be an asynchronous loading function, or an options object that more specifically customizes the loading behavior.

function  defineAsyncComponent (
  source: AsyncComponentLoader | AsyncComponentOptions
): Component 
type  AsyncComponentLoader = () =>  Promise < Component >
 interface  AsyncComponentOptions {
   loader : AsyncComponentLoader 
  loadingComponent?: Component 
  errorComponent?: Component 
  delay?: number 
  timeout?: number 
  suspensible?: boolean 
  onError?: ( 
    error: Error ,
    retry: () => void ,
    fail: () => void ,
    attempts: number
   ) =>  any 
}

defineAsyncComponent() – official website example

< script  setup > import { defineAsyncComponent } from 'vue'
 

const  AsyncComp = defineAsyncComponent ( () => {
   return  new  Promise ( ( resolve, reject ) => {
     resolve ( /* component obtained from the server */ )
  })
})

const  AdminPage = defineAsyncComponent ( () => 
  import ( './components/AdminPageComponent.vue' )
)
</ script > 
< template > 
  < AsyncComp /> 
  < AdminPage /> 
</ template >

ESModule dynamic import will also return one Promise, so in most cases we will defineAsyncComponentuse it with . Build tools like Viteand Webpackalso support this syntax (and will use them as code splitting points when packaging), so we can also use it to import Vuesingle-file components.

defineAsyncComponent() – VitePress Example

<script setup lang= "ts" >
 import { defineAsyncComponent } from  'vue' 
import  type { DefaultTheme } from  'vitepress/theme' 
defineProps<{ carbonAds : DefaultTheme . CarbonAdsOptions }>()
 const  VPCarbonAds = __CARBON__
  ? defineAsyncComponent ( () =>  import ( './VPCarbonAds.vue' ))
  : () =>  null
</script>
< template > 
  < div  class = "VPDocAsideCarbonAds" > 
    < VPCarbonAds  :carbon-ads = "carbonAds" /> 
  </ div > 
</ template >

defineAsyncComponent()scenes to be used:

  1. When you need to load certain components asynchronously, you can use defineAsyncComponentto lazy load components, which can improve application performance.
  2. In some complex pages, some components may only be used when the user performs a specific operation or enters a specific page. Use defineAsyncComponentcan reduce resource overhead during initial page load.
  3. It can also be used when you need to load certain components dynamically defineAsyncComponent. For example, loading different components based on different paths in routing.

In Vue3addition to, many Vue 3libraries and frameworks based on have also begun to use defineAsyncComponentto implement asynchronous loading of components. For example:

  • VitePress¬†:¬†ViteThe official documentation tool, uses¬†defineAsyncComponentto implement asynchronous loading of documentation pages.
  • Nuxt.js¬†: Static website generator based on Vue.js, supported starting from version 2.15¬†defineAsyncComponent.
  • Quasar Framework¬†:¬†Vue.jsUI framework based on Quasar, supported starting from version 2.0¬†defineAsyncComponent.
  • Element UI Plus¬†:¬†Vue 3A UI library based on , which uses¬†defineAsyncComponentto implement asynchronous loading of components.

In short, with the popularity of Vue 3, more and more libraries and frameworks are beginning to use it defineAsyncComponentto improve application performance.

defineCustomElement()

This method defineComponentaccepts the same parameters as , except that it returns a constructor of a native custom element class .

function  defineCustomElement (
  component:
    | (ComponentOptions & { styles?: string [] })
    | ComponentOptions[ 'setup' ]
 ): {
   new (props?: object ): HTMLElement 
}

In addition to the regular component options, defineCustomElement()a special option is supported styles, which should be an array of inline CSSstrings, and the provided ones CSSwill be injected into the element shadow root.
The return value is a customElements.define()custom element constructor that can be registered via .

import { defineCustomElement } from  'vue' 
const  MyVueElement = defineCustomElement ({
   /* component options*/
})
// Register custom elements 
customElements. define ( 'my-vue-element' , MyVueElement )

Build custom elements using Vue

import { defineCustomElement } from  'vue'

const  MyVueElement = defineCustomElement ({
   // Here are the usual Vue component options 
  props : {},
   emits : {},
   template : `...` ,
   // defineCustomElement-specific: CSS 
  styles injected into the shadow root : [ `/* inlined css */` ],
})
// Register custom elements 
// After registration, all `<my-vue-element>` tags in this page 
// will be upgraded 
customElements. define ( 'my-vue-element' , MyVueElement )
 // You can also Programmatically instantiate elements: 
// (must be after registration) 
document . body . appendChild (
   new  MyVueElement ({
     // Initialize props (optional)
  })
)
// Component uses 
<my-vue-element></my-vue-element>

In addition Vue 3to , some Vue 3libraries and frameworks based on have also begun to use defineCustomElementto Vuepackage components into custom elements for use by other frameworks or pure HTML pages. For example:

  • Ionic Framework¬†:¬†Web ComponentsA mobile UI framework based on Ionic. Starting from version 6, it supports¬†defineCustomElementpackaging¬†Ioniccomponents into custom elements.
  • LitElement¬†: A library launched by Google¬†Web Componentsthat provides¬†Vuetemplate syntax similar to and supports¬†defineCustomElementpackaging¬†LitElementcomponents into custom elements.
  • Stencil¬†: A tool chain¬†Ionic Teamdeveloped by Stencil¬†Web Componentsthat can convert components of any framework into custom elements and supports¬†defineCustomElementdirect¬†Vuepackaging of components into custom elements.

In short, with Web Componentsthe continuous popularity and development of , more and more libraries and frameworks are beginning to use defineCustomElementto achieve cross-framework and cross-platform component sharing.

summary

This time we focus on Vue3several APIs related to dependency injection and component definition in , learn their basic usage, and analyze usage scenarios combined with currently popular libraries and frameworks to deepen our understanding of them.

The content is included in the github repository