The thing related to dependency injection and component definition in Vue 3

Let's talk about dependency injection in Vue 3 related to component definition.

main content

In this sharing, we mainly cover the following:

  • 📝 provide() & inject() - dependency injection
  • 🛠 nextTick() - after the next DOM update cycle
  • 🎨 Component definition
    • defineComponent() - Component definition type deduction helper function
    • defineAsyncComponent() - an 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:

  • The key to inject, string or Symbol;
export interface InjectionKey<T> extends Symbol {}
  • Corresponding to the injected value

Similar to the API for registering lifecycle hooks, provide() must be called synchronously during the setup() phase of the component.

inject()

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

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

// with default
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 the injected key. Vue will traverse the chain of parent components and match the key to determine the provided value. If multiple components in the chain of parent components provide values ​​for the same key, the closer components will "overwrite" the values ​​provided by components further up the chain. If no value can be matched by key, inject() will return undefined unless a default value is provided.

  • The second parameter is optional, which is the default value used when no key is matched. It can also be a factory function that returns some value that is more complicated to create. If the default value is itself a function, then you must pass false as the third argument, 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 reactive values
  const count = ref(0) provide('count', count) // Provide Symbol as key
  provide(fooSymbol, count)
</script>
// inject
<script setup>
import { inject } from 'vue'
import { fooSymbol } from './injectionSymbols'

// The default way to inject values
const foo = inject('foo')

// Inject reactive values
const count = inject('count')

// Injection via Symbol type key
const foo2 = inject(fooSymbol)

// Injects a value, or uses the provided default if empty
const bar = inject('foo', 'default value')

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

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

provide() & inject() - ElementUI Plus example Breadcrumb components

<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 the 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 uses

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 refresh.

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

Explanation: When you change the responsive state in Vue, the final DOM update is not synchronously effective, but Vue caches them in a queue until the next "tick" to execute together. This is to ensure that each component is only updated once, no matter how many state changes occur.

nextTick() can be used immediately after a state change to wait for the DOM update to complete. You can pass a callback function as an argument, or await the returned Promise.

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()
  // The DOM has now been updated
  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. When you need to operate on the DOM immediately after modifying some data, you can use nextTick to ensure that the DOM has been updated. For example, when using $ref to get an element, you need to ensure that the element has been rendered to get it correctly.

  2. In some complex pages, some components may change frequently due to conditional rendering or dynamic data. Use nextTick to avoid frequent DOM manipulations and improve application performance.

  3. When you need to access some computed properties or listener values ​​in the template, you can also use nextTick to ensure that these values ​​have been updated. This avoids accessing old values ​​in views.

In short, nextTick is a very useful API that can ensure that the DOM is operated on at the right time, avoid some unnecessary problems, and can improve the performance of the application.

defineComponent()

Helper functions that provide type inference when defining Vue 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 doesn't actually do anything at runtime other than to provide type deduction.

Note that the type of the return value is a bit special: it will be a constructor type whose instance type is the component instance type inferred from the options. This is to enable type inference support for this return value when used as a tag in TSX.

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

refer to: 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 a side effect by some build tools, such as webpack. Even if a component is never used, it may not be tree-shake.

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

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

Please note that if you are using Vite in your project, you don't need to do this, because Rollup (the production environment packaging tool used under the hood of Vite) can intelligently determine that defineComponent() does not actually have side effects, so there is no need to manually comment.

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()

Defines an asynchronous component that is lazy loaded at runtime. The argument can be an asynchronous loading function, or an options object for more specific customization of 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(/* Components retrieved from the server */)
  })
})

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

ES module dynamic import also returns a Promise, so in most cases we will use it with defineAsyncComponent. Build tools like Vite and Webpack also support this syntax (and use them as code split points when bundling), so we can use it to import Vue single-file components as well.

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() usage scenario:

  1. When you need to load some components asynchronously, you can use defineAsyncComponent for lazy loading of components, which can improve the performance of the application.
  2. In some complex pages, some components may only be used when the user performs a specific operation or enters a specific page. Using defineAsyncComponent can reduce resource overhead on initial page load.
  3. You can also use defineAsyncComponent when you need to dynamically load some components. For example, load different components according to different paths in the route.

In addition to Vue3, many Vue 3-based libraries and frameworks have also begun to use defineAsyncComponent to achieve asynchronous loading of components. For example:

  • VitePress: Vite's official document tool, using defineAsyncComponent to achieve asynchronous loading of document pages.
  • Nuxt.js: A Vue.js based static site generator that supports defineAsyncComponent since version 2.15.
  • Quasar Framework: A Vue.js-based UI framework that supports defineAsyncComponent since version 2.0.
  • Element UI Plus: A UI library based on Vue 3, using defineAsyncComponent to implement asynchronous loading of components.

In conclusion, with the popularity of Vue 3, more and more libraries and frameworks are starting to use defineAsyncComponent to improve the performance of the application.

defineCustomElement()

This method accepts the same parameters as defineComponent, the difference is that it will return a constructor of the native custom element class.

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

In addition to the regular component options, defineCustomElement() also supports a special option styles, which should be an array of inline CSS strings, and the provided CSS will be injected into the shadow root of the element.
The return value is a custom element constructor that can be registered with customElements.define().

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

Build custom elements with Vue

import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
  // Here are the Vue component options as usual
  props: {},
  emits: {},
  template: `...`,
  // defineCustomElement specific: CSS injected into shadow root
  styles: [`/* 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 instantiate elements programmatically:
// (must be after registration)
document.body.appendChild(
  new MyVueElement({
    // Initialize props (optional)
  })
)
// Component usage
<my-vue-element></my-vue-element>

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

  • Ionic Framework: A mobile UI framework based on Web Components. Starting from version 6, it supports using defineCustomElement to package Ionic components into custom elements.
  • LitElement: A Web Components library launched by Google that provides a Vue-like template syntax and supports packaging LitElement components into custom elements using defineCustomElement.
  • Stencil: A Web Components toolchain developed by Ionic Team that can convert components of any framework into custom elements, and supports using defineCustomElement to directly package Vue components into custom elements.

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

summary

This time, we will focus on several API s related to dependency injection and component definition in Vue3, learn their basic usage methods, and analyze usage scenarios in combination with currently popular libraries and frameworks, so as to deepen our understanding of them.

The content is included in github repository

Tags: Front-end Javascript Vue.js

Posted by phpnewbiy on Sun, 19 Mar 2023 00:52:32 +1030