How to Build a Cross‑Version Vue Component Library for Vue 2 and Vue 3

This article explains the challenges of upgrading large B2B Vue 2 projects to Vue 3, evaluates three technical approaches—Vue SFC templates, render functions, and standard JSX—and details the chosen JSX‑based strategy, multi‑layer architecture, TypeScript support, and open‑source implementation for a seamless cross‑version component library.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
How to Build a Cross‑Version Vue Component Library for Vue 2 and Vue 3

Enterprise projects often remain on Vue 2 while Vue 3 has been stable for years. To avoid costly, risky rewrites, a progressive upgrade strategy based on micro‑frontend architecture was adopted: existing Vue 2 applications stay unchanged, and new modules are built with the latest stack.

Decision Options

Vue SFC / Template

Advantages

Templates are the primary Vue syntax; learning curve is low.

Static compilation can produce version‑specific bundles, keeping bundle size small and preserving Vue’s template optimizations.

Relies on standard syntax, no need for framework hacks.

Disadvantages

Build pipeline must emit two separate bundles.

Subtle syntax differences (e.g., v-model changes, removed .sync) become hard to handle.

Template is locked to Vue 3 syntax, preventing use of new Vue 3 features such as defineModel or defineOptions.

Render Functions Both Vue 2 and Vue 3 support render functions, but their APIs diverge significantly. The team decided not to pursue this path.

Standard JSX

Advantages

No extra Babel plugins; mainstream compilers (tsc, swc, esbuild) can compile directly.

Runtime transforms JSX into version‑specific render functions.

TypeScript‑friendly; works without Vue‑specific plugins.

Development experience similar to React JSX.

Disadvantages

Cannot leverage Vue template compilation optimizations (static hoisting, caching, etc.).

Runtime abstraction adds a small performance overhead.

Implementation requires deep knowledge of Vue internals and may produce less readable code compared with React‑style APIs.

The JSX approach was selected because it is simple to implement and offers better control for future version updates.

Architecture Strategy

Cross‑version component libraries must handle more than syntax differences: API changes, rendering functions, routing, and i18n also vary. The solution is organized into five layers:

API Layer – Uses vue-demi to expose a unified Composition API and defineComponent that work in both Vue 2 and Vue 3.

View‑Syntax Layer – Provides a jsx-runtime that normalizes render‑function differences between versions.

Adapter Layer – Implements adapters for third‑party libraries (e.g., Element UI vs. Element Plus) to expose a consistent interface.

Component‑Library Layer – Builds UI components on top of the abstractions above.

Application Layer – Consuming Vue 2 or Vue 3 apps dynamically select the appropriate adapters at runtime.

Implementation policies:

New‑Version‑First – Prefer Vue 3 features (Composition API, defineComponent, JSX aligned with Vue 3 render functions).

Shortcoming‑First – Accept that some Vue 3 features (Fragment, Teleport, Suspense, await setup) cannot be back‑ported to Vue 2.

Fallback – Use vue-demi 's isVue2 flag to branch code where differences cannot be abstracted.

Implementation Details

API Compatibility with vue-demi

vue-demi

runs a post‑install hook that detects the installed Vue version and exports the appropriate symbols: <=2.6: exports vue + @vue/composition-api. 2.7: exports the built‑in Composition API from Vue 2.7. >=3.0: exports the native Vue 3 API and shims Vue 2 set / del APIs.

The team forked vue-demi to add extra shims required by their library.

JSX Runtime

The JSX runtime translates JSX syntax into version‑specific render functions. The core idea is a thin wrapper that, at build time, generates two bundles (Vue 2 and Vue 3) and, at runtime, selects the appropriate render function based on the detected Vue version.

TypeScript Support and declareComponent

To simplify type definitions, a wrapper declareComponent was created around defineComponent. It drops the Options API, keeping only the Composition API, and provides:

Strongly typed props, emit, expose, and slots via generic parameters.

Conversion of Vue event names to on* props for JSX ergonomics.

Explicit handling of inheritAttrs and runtime v‑slots definitions.

Example:

const Counter = declareComponent({
  name: 'Counter',
  props: declareProps<{ initialValue: number }>(['initialValue']),
  emits: declareEmits<{ change: (value: number) => void }>(),
  setup(props, { emit }) {
    const count = ref(props.initialValue)
    const handleClick = () => {
      count.value++
      emit('change', count.value)
    }
    return () => (
      <div title="count" onClick={handleClick}>count: {count.value}</div>
    )
  }
})

Generic component support is achieved by asserting the component’s constructor type; current Volar support is limited.

Element Adapter

The adapter mirrors vue-demi 's post‑install logic: it selects element-ui for Vue 2 and element-plus for Vue 3. Most components are re‑exported directly; components with changed prop names are wrapped to normalize the API.

export { Button } from 'element-ui'

import { TimePicker as ElTimePicker } from 'element-ui'
import { h } from '@wakeadmin/h'
import { normalizeDateFormat } from '../../shared/date-format'

export const TimePicker = {
  functional: true,
  render(_, context) {
    const { format, selectableRange, valueFormat, ...other } = context.props
    other.pickerOptions = {
      ...other.pickerOptions,
      format: format && normalizeDateFormat(format),
      selectableRange,
    }
    if (valueFormat) {
      other.valueFormat = normalizeDateFormat(valueFormat)
    }
    return h(ElTimePicker, Object.assign({}, context.data, { props: other, attrs: undefined }), context.slots())
  }
}

Router Adapter

Vue Router’s API is similar across versions, so the adapter simply retrieves the router instance from the component context, handling the slight shape differences.

export interface RouteLike {
  query: Record<string, any>
  params: Record<string, any>
  hash: string
  path: string
}

export type RouteLocation = string | {
  query?: Record<string, any>
  hash?: string
  path?: string
  name?: string
  params?: Record<string, any>
  replace?: boolean
}

export interface RouterLike {
  push(to: RouteLocation): Promise<any>
  replace(to: RouteLocation): Promise<any>
  back(): void
  forward(): void
  go(delta: number): void
}

export function useRouter() {
  const instance = getCurrentInstance()
  if (isVue2) {
    return (instance?.proxy?.$root as { $router: RouterLike } | undefined)?.$router
  } else {
    return (instance?.root?.proxy as unknown as { $router: RouterLike })?.$router
  }
}

Open‑Source Release

The full implementation—including the JSX runtime, component library, element‑adapter, and router‑adapter—has been open‑sourced. Developers can clone the repository, submit pull requests, and star the project.

Summary

A JSX‑based conversion layer abstracts Vue 2 and Vue 3 rendering differences, allowing a single component library to serve both versions. Most adapters follow the vue-demi pattern: provide a unified API and switch the concrete implementation at install time.

Further Reading

Vue 2/3 render‑function differences and compatibility solutions

Feature: generic support for defineComponent vite-plugin-vue: https://github.com/vitejs/vite-plugin-vue

vite-plugin-vue2: https://github.com/vitejs/vite-plugin-vue2

babel-plugin-jsx (Vue JSX Babel plugin): https://github.com/vuejs/babel-plugin-jsx

jsx-vue2 (JSX for Vue 2): https://github.com/vuejs/jsx-vue2

Vue language tools: https://github.com/vuejs/language-tools

vue-loader: https://github.com/vuejs/vue-loader

Component flow diagram
Component flow diagram
JSX compilation flow
JSX compilation flow
Layered architecture diagram
Layered architecture diagram
Animated illustration
Animated illustration
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptVueComponent LibraryJSXCross-Versionvue-demi
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.