Vue To Umijs

Migrate Vue 2/3 projects to React on UmiJS (@umijs/max): conventions, constraints, stack mapping, and syntax examples (single SKILL.md). Use for .vue → Umi pages and data flow.

Audits

Pass

Install

openclaw skills install vue2umijs

Vue → UmiJS + React migration

Goal: move a Vue codebase to UmiJS (@umijs/max) + React 18 + antd, with behavior parity and Umi’s routing, data flow, and folder conventions.

Document layout: rules and constraints first; Stack and Umi mapping next; Syntax examples last.

Scope

  • Source: Vue 2 / 3 (Options API, Composition API, SFC).
  • Target: Umi 4 + @umijs/max + React 18; function components and hooks; TypeScript for public APIs.
  • Default stack: antd; src/models + useModel; React Router v6 (via Umi); Less + *.module.less; pages as PageName/index.tsx + PageName/index.module.less.

Principles

  1. Parity first: routing, URL, guards, loading/errors, and UX before micro-optimizations.
  2. Single source of truth: no duplicate domain state across store and local state unless there is an explicit draft/edit buffer.
  3. Side-effect boundaries: useEffect (or Umi equivalents) for subscriptions and async with cleanup; cancel in-flight work with AbortController where needed.
  4. No Vue runtime: replace plugins, mixins, filters, and global buses with hooks, Umi models, and explicit modules.
  5. Closures and deps: dependency arrays reflect real deps; derive with render/useMemo, not effects for pure derivation.

API mapping (summary)

AreaDirection
Page .vuePageName/index.tsx + index.module.less
Child .vueFoo.tsx + Foo.module.less (same folder, matching basename)
Component APIdefineComponentfunction Name(props: NameProps); emitonXxx
ref / reactive / computed / watchuseState / useRef, useReducer, useMemo, useEffect (deps + cleanup)
Vue RouterUmi config / routes; useNavigate, useParams, etc. — see Routing and data below
Pinia / Vuexsrc/models + useModel by domain; prefer useRequest for local fetch/cache
Element UI / Element PlusMap to antd with matching interaction and validation semantics

Constraints

  1. One router tree: use Umi nested routes/layouts; do not add a standalone BrowserRouter beside Umi; avoid full-page navigations that break SPA where the old app used in-app routing.
  2. Typing: avoid any on exported components, route params, and model shapes.
  3. Security and a11y: no untrusted dangerouslySetInnerHTML; preserve focus, labels, and keyboard behavior; env vars and URL semantics stay aligned or are explicitly documented.
  4. Styles: index.module.less next to index.tsx for pages; child components: .tsx + .module.less with the same basename.
  5. Vue-specific features: custom directives, plugins, defineExpose + parent refs must map to explicit React/Umi patterns (controlled props, forwardRef/useImperativeHandle, permission guards)—never silently drop behavior.
  6. Async and lists: clean up listeners and requests on unmount or dep changes; use stable ids for list keys, not indexes when order changes.
  7. Do not: run side effects during render; use useEffect for pure derivation; recreate EventBus / mixin / filter pipelines; mirror props to state without need; introduce a second global state stack for the same domain without a plan (prefer Umi models).

Additional constraints (often missed—verify explicitly)

  1. i18n: migrate vue-i18n keys/namespaces and locale switching to the chosen solution (including Umi locale plugins); avoid leftover hard-coded copy or mixed languages.
  2. HTTP and errors: use the shared request wrapper (and @umijs/max request APIs) so interceptors, error codes, auth headers, and global toasts match the old Vue layer; avoid ad-hoc fetch that bypasses global handling.
  3. Environment: map VITE_* / import.meta.env to Umi env / define; never ship secrets or internal-only URLs in the client bundle.
  4. Forms and tables: align Element validation rules, async validation, and blur/submit timing with antd Form; keep Table rowKey, pagination, sort, and filter params aligned with backend contracts; validate dynamic forms (Form.List, etc.) behavior-by-behavior.
  5. API scalars: keep explicit conversion between backend 0/1, stringly numbers, enums, and UI booleans—prefer normalization at submit/response boundaries to avoid silent drift.
  6. Build and deploy: publicPath / base, static asset URLs, and CDN prefixes match the old site or are documented; avoid production 404s for assets.
  7. Dev / integration: move mocks and dev proxy from the Vue setup into Umi conventions (mock/, proxy) so local and joint-debug environments stay consistent.
  8. SSR vs CSR: with Umi SSR, do not use window / document / localStorage on the server render path; with CSR-only, document any first-screen / SEO differences vs the legacy app.

Pre-release checklist

  • Routing, query, and auth behavior matches the legacy app (or differences are documented).
  • useEffect deps are complete; async work is cancelled or ignored when stale.
  • Pages include index.tsx + index.module.less; child style files match basename.
  • Custom directives/plugins/exposed methods have a mapped implementation or migration note.
  • Request layer, env vars, and deploy paths checked against Additional constraints.
  • Sample pass on critical forms/tables and i18n paths vs legacy behavior.

Style and structure

  • Prefer function components and hooks; emitonXxx.
  • Keep feature boundaries similar to the Vue repo; page folder naming: see Vue SFC styles → Umi components below.

Stack and Umi mapping

Umi/antd APIs follow the official docs.

Target stack

LayerChoice
App framework@umijs/max (Umi 4)
UIAnt Design (antd)
Global stateUmi data flow: src/models + useModel
RuntimeReact 18
RoutingReact Router v6 (via Umi; configure in config + routes)

Styling: Less + CSS Modules (*.module.less). Pages: PageName/index.tsx + PageName/index.module.less.

Routing and data (Umi)

Typical Vue patternUmi + React
Fetch in onMounted after navigationuseRequest, useModel, or useEffect + project request in page/layout
beforeEach authaccess, route wrappers, or layout-level guards (see Umi docs)
Navigation / URL paramsuseNavigate, useParams, useSearchParams, useLocation from @umijs/max or umi

Do not add a second standalone BrowserRouter root outside Umi.

Pinia / Vuex → Umi data flow

VueUmi
Modular storesrc/models split by domain + useModel
Getters / derivedLogic in models or useMemo in components
Async actionsModel effects, or useRequest in pages then update model

Prefer useRequest for local request/cache when global state is not needed.

Vue SFC styles → Umi components

VueReact (Umi)
Page <style scoped>PageDir/index.tsx + index.module.less, import styles from './index.module.less'
Child scopedFoo.tsx + Foo.module.less in the same folder
<style lang="less">Same content in the matching *.module.less

For HTTP, i18n, env, deploy, and mock constraints, see Additional constraints above.

Syntax examples

Vue vs React syntax; use @umijs/max, antd, and the rules above in real pages.

1. Counter

Vue 3 (<script setup>)

<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function inc() {
  count.value++
}
</script>
<template>
  <button type="button" @click="inc">{{ count }}</button>
</template>

React

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  return (
    <button type="button" onClick={() => setCount((c) => c + 1)}>
      {count}
    </button>
  )
}

2. Conditional list

Vue

<template>
  <ul v-if="items.length">
    <li v-for="item in items" :key="item.id">{{ item.name }}</li>
  </ul>
  <p v-else>No data</p>
</template>

React

return items.length ? (
  <ul>
    {items.map((item) => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
) : (
  <p>No data</p>
)

3. Controlled input

Vue

<input v-model="text" />

React

const [text, setText] = useState('')
<input value={text} onChange={(e) => setText(e.target.value)} />

4. Child events (emit)

Vue

<script setup lang="ts">
const emit = defineEmits<{ (e: 'update', v: number): void }>()
function notify() {
  emit('update', 1)
}
</script>

React

type Props = { onUpdate: (v: number) => void }
export function Child({ onUpdate }: Props) {
  function notify() {
    onUpdate(1)
  }
}

5. computed and watch

Vue

const doubled = computed(() => count.value * 2)
watch(count, (v) => console.log(v))

React

const doubled = useMemo(() => count * 2, [count])
useEffect(() => {
  console.log(count)
}, [count])

6. Provide / inject

Vue

provide('theme', 'dark')
const theme = inject('theme')

React

import { createContext, useContext, useMemo, useState } from 'react'

type Theme = 'light' | 'dark'
type ThemeContextValue = {
  theme: Theme
  setTheme: (v: Theme) => void
}

const ThemeContext = createContext<ThemeContextValue | null>(null)

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light')
  const value = useMemo(() => ({ theme, setTheme }), [theme])
  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}

export function useTheme() {
  const ctx = useContext(ThemeContext)
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
  return ctx
}

// usage:
// const { theme, setTheme } = useTheme()

7. Cleanup

Vue

onUnmounted(() => window.removeEventListener('resize', onResize))

React

useEffect(() => {
  window.addEventListener('resize', onResize)
  return () => window.removeEventListener('resize', onResize)
}, [])

8. Scoped slot → render prop

Vue

<Child v-slot="{ row }">
  <span>{{ row.name }}</span>
</Child>

React

<Child renderRow={(row) => <span>{row.name}</span>} />

9. Route params

Vue

const route = useRoute()
const id = route.params.id as string

Umi (same as React Router v6)

import { useParams } from '@umijs/max'
// or import { useParams } from 'umi'

const { id } = useParams<{ id: string }>()

10. React mental model: closure and deps

Common but not recommended

// stale closure: interval always sees initial count
useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1)
  }, 1000)
  return () => clearInterval(timer)
}, []) // count is missing

Recommended

// use functional update to avoid stale closure
useEffect(() => {
  const timer = setInterval(() => {
    setCount((c) => c + 1)
  }, 1000)
  return () => clearInterval(timer)
}, [])

Common but not recommended

// derives data via effect + extra state
const [fullName, setFullName] = useState('')
useEffect(() => {
  setFullName(`${user.firstName} ${user.lastName}`)
}, [user.firstName, user.lastName])

Recommended

// derive directly in render / useMemo
const fullName = useMemo(
  () => `${user.firstName} ${user.lastName}`,
  [user.firstName, user.lastName]
)