Install
openclaw skills install vue2umijsMigrate 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.
openclaw skills install vue2umijsGoal: 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.
src/models + useModel; React Router v6 (via Umi); Less + *.module.less; pages as PageName/index.tsx + PageName/index.module.less.useEffect (or Umi equivalents) for subscriptions and async with cleanup; cancel in-flight work with AbortController where needed.useMemo, not effects for pure derivation.| Area | Direction |
|---|---|
Page .vue | PageName/index.tsx + index.module.less |
Child .vue | Foo.tsx + Foo.module.less (same folder, matching basename) |
| Component API | defineComponent → function Name(props: NameProps); emit → onXxx |
ref / reactive / computed / watch | useState / useRef, useReducer, useMemo, useEffect (deps + cleanup) |
| Vue Router | Umi config / routes; useNavigate, useParams, etc. — see Routing and data below |
| Pinia / Vuex | src/models + useModel by domain; prefer useRequest for local fetch/cache |
| Element UI / Element Plus | Map to antd with matching interaction and validation semantics |
BrowserRouter beside Umi; avoid full-page navigations that break SPA where the old app used in-app routing.any on exported components, route params, and model shapes.dangerouslySetInnerHTML; preserve focus, labels, and keyboard behavior; env vars and URL semantics stay aligned or are explicitly documented.index.module.less next to index.tsx for pages; child components: .tsx + .module.less with the same basename.defineExpose + parent refs must map to explicit React/Umi patterns (controlled props, forwardRef/useImperativeHandle, permission guards)—never silently drop behavior.keys, not indexes when order changes.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).vue-i18n keys/namespaces and locale switching to the chosen solution (including Umi locale plugins); avoid leftover hard-coded copy or mixed languages.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.VITE_* / import.meta.env to Umi env / define; never ship secrets or internal-only URLs in the client bundle.Form; keep Table rowKey, pagination, sort, and filter params aligned with backend contracts; validate dynamic forms (Form.List, etc.) behavior-by-behavior.0/1, stringly numbers, enums, and UI booleans—prefer normalization at submit/response boundaries to avoid silent drift.publicPath / base, static asset URLs, and CDN prefixes match the old site or are documented; avoid production 404s for assets.mock/, proxy) so local and joint-debug environments stay consistent.window / document / localStorage on the server render path; with CSR-only, document any first-screen / SEO differences vs the legacy app.useEffect deps are complete; async work is cancelled or ignored when stale.index.tsx + index.module.less; child style files match basename.emit → onXxx.Umi/antd APIs follow the official docs.
| Layer | Choice |
|---|---|
| App framework | @umijs/max (Umi 4) |
| UI | Ant Design (antd) |
| Global state | Umi data flow: src/models + useModel |
| Runtime | React 18 |
| Routing | React Router v6 (via Umi; configure in config + routes) |
Styling: Less + CSS Modules (*.module.less). Pages: PageName/index.tsx + PageName/index.module.less.
| Typical Vue pattern | Umi + React |
|---|---|
Fetch in onMounted after navigation | useRequest, useModel, or useEffect + project request in page/layout |
beforeEach auth | access, route wrappers, or layout-level guards (see Umi docs) |
| Navigation / URL params | useNavigate, useParams, useSearchParams, useLocation from @umijs/max or umi |
Do not add a second standalone BrowserRouter root outside Umi.
| Vue | Umi |
|---|---|
| Modular store | src/models split by domain + useModel |
| Getters / derived | Logic in models or useMemo in components |
| Async actions | Model effects, or useRequest in pages then update model |
Prefer useRequest for local request/cache when global state is not needed.
| Vue | React (Umi) |
|---|---|
Page <style scoped> | PageDir/index.tsx + index.module.less, import styles from './index.module.less' |
| Child scoped | Foo.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.
Vue vs React syntax; use @umijs/max, antd, and the rules above in real pages.
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>
)
}
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>
)
Vue
<input v-model="text" />
React
const [text, setText] = useState('')
<input value={text} onChange={(e) => setText(e.target.value)} />
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)
}
}
computed and watchVue
const doubled = computed(() => count.value * 2)
watch(count, (v) => console.log(v))
React
const doubled = useMemo(() => count * 2, [count])
useEffect(() => {
console.log(count)
}, [count])
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()
Vue
onUnmounted(() => window.removeEventListener('resize', onResize))
React
useEffect(() => {
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [])
Vue
<Child v-slot="{ row }">
<span>{{ row.name }}</span>
</Child>
React
<Child renderRow={(row) => <span>{row.name}</span>} />
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 }>()
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]
)