Guía de migración
Esta página trata dos cosas: el mapeo general de conceptos de Vue (lh-ui-plus / lh-ui-vue2) a React, y las diferencias intencionales de esta biblioteca respecto a la versión de Vue.
Tabla general de mapeo de conceptos
| Vue (lh-ui-plus) | React (@lh-ui/react) |
|---|---|
defineProps<T>() | interface LhXxxProps + parámetros por defecto en la deestructuración |
withDefaults(...) | valores por defecto en la deestructuración (sin defaultProps) |
emits + emit('change', [val, e]) | onChange?: (value, event) => void |
v-model="x" | value={x} onChange={...} |
v-model:visible="x" | visible={x} onVisibleChange={...} |
defineExpose({ ... }) | forwardRef + useImperativeHandle |
<slot> | children |
<slot name="footer"> | prop footer?: ReactNode |
slot con nombre #header | prop header?: ReactNode |
provide(...) / inject(...) | Context.Provider / useContext |
provide debe usar computed | el valor del Context debe usar useMemo |
computed(() => ...) | useMemo(() => ..., [deps]) |
watch(src, cb) | useEffect(() => cb(), [src]) |
watch(src, cb, { immediate: true }) | useEffect (los effects se ejecutan una vez de inmediato por naturaleza) |
onMounted / onUnmounted | setup / cleanup del useEffect |
<Transition name="lh-x-fade"> | useTransition(visible, duration) impulsa los mismos nombres de clase |
<Teleport to="body"> | createPortal(..., document.body) |
ref="<name>" | useRef<HTMLDivElement>(null) |
nextTick() | flushSync() o un effect (normalmente no hace falta) |
app.use(LhUI) | no existe: las exportaciones con nombre se importan bajo demanda |
<LhXxx v-bind="obj"> | <LhXxx {...obj}> |
Controlado y no controlado
El par modelValue / update:modelValue de Vue se desdobra en dos conjuntos de props en React:
<!-- Vue -->
<LhInput v-model="text" />
// React: controlado
<LhInput value={text} onChange={(val) => setText(val)} />
// React: no controlado
<LhInput defaultValue="Valor inicial" />
Usar ambos a la vez (pasar value y defaultValue simultáneamente) emite una advertencia en desarrollo, igual que la semántica de los formularios nativos de React.
Callbacks de eventos
Todas las props de evento siguen la convención onXxx, y las firmas de callback usan uniformemente dos parámetros (value, event):
<!-- Vue -->
<LhSelect @change="onChange" />
// React
<LhSelect onChange={(value, option) => console.log(value, option)} />
Animaciones de transición
El <Transition> de Vue es declarativo; la versión de React usa el hook interno useTransition para impulsar el mismo juego de nombres de clase:
import { useTransition } from '@lh-ui/react'
const { mounted, phase } = useTransition(visible, 300)
// Nombres de clase: `lh-dialog-fade-${phase}` — phase ∈ enter-from / enter-active / enter-to
Los nombres de clase son exactamente los mismos que en el SCSS de share, de modo que el resultado visual es idéntico fotograma a fotograma al de la versión de Vue.
Capas flotantes y portales
El <Teleport to="body"> de Vue corresponde al createPortal de React: Dialog, el desplegable de Select y Tooltip se montan todos en document.body. La geometría de posicionamiento la calcula useLhPopper (comparte @lh-ui/share/core/shared/popper con la versión de Vue).
ref imperativo
El defineExpose de Vue corresponde a forwardRef + useImperativeHandle:
<!-- Vue -->
<LhForm ref="formRef" />
// React
const formRef = useRef<LhFormInstance>(null)
<LhForm ref={formRef} />
<formRef.current?.validate() />
Configuración global
<!-- Vue -->
<LhConfigProvider :size="size">
<App />
</LhConfigProvider>
// React
<LhConfigProvider size={size}>
<App />
</LhConfigProvider>
Diferencias de implementación con la versión de Vue
Las siguientes diferencias son ajustes intencionales propios de la forma React, que conviene tener en cuenta al migrar. Cada una está registrada en el repositorio.
1. Inyección de estilos bajo demanda
A diferencia de lh-ui-plus (que inyecta la hoja completa), esta biblioteca se alinea con lh-ui-vue2: la entrada del paquete no inyecta estilos; el usuario importa explícitamente theme.css + style/<name>.
2. Cadena de construcción como vue2
vite lib mode + preserveModules + tsc emite los d.ts directamente (plus usa un escaneo multientrada con rollup). preserveModules admite de forma natural la importación bajo demanda, tsx no tiene módulos virtuales de SFC y tsc emitiendo d.ts directamente es lo más sencillo.
3. Respaldo del lado consumidor de ConfigProvider
La versión de Vue usa inject(..., null) y cada componente comprueba si el valor es nulo. El hook consumidor de la versión de React devuelve un objeto de respaldo con todos los campos undefined, de modo que los componentes no necesitan la comprobación: es una práctica habitual con React Context, ya que useContext no permite suministrar un valor por defecto en el punto de llamada.
4. Diferencias de montaje de notification / loading
Los tres son servicios imperativos, pero el montaje difiere: message / notification comparten un contenedor singleton (un createRoot perezoso + canal de acciones), mientras que loading debe montarse en cualquier elemento objetivo (a pantalla completa o local), mediante un createRoot por instancia, donde cada instancia mantiene su propio root y sus manejadores de restauración de overflow / inert.
La API de notification es de tipo objeto (notification.success({ title, content }), alineada con el Notification(options) de plus) y también admite una forma abreviada de cadena (se renderiza únicamente la línea del título). La animación FLIP de reubicación no está implementada: la clase -move que proporciona el transition-group de Vue no tiene un equivalente nativo en React, de modo que, al añadir o quitar elementos de la cola, las tarjetas hermanas saltan instantáneamente de posición.
5. onChange de Input con dos parámetros
La versión de Vue separa @input / @change; la versión de React los unifica en onChange(value, event) (además existe onInput, que transmite el evento nativo de entrada).
6. autosize de Input lo gestiona el componente sobre el DOM
La versión de Vue calcula la altura en la capa de DOM mediante un atributo data-autosize y una directiva. La versión de React lee scrollHeight directamente en useEffect y escribe en textarea.style.height (sujeto a minRows / maxRows), evitando introducir un mecanismo de directivas adicional.
7. Select pasa a usar useLhPopper para el posicionamiento
Algunas versiones de Vue de Select posicionan el desplegable con un fixed escrito a mano, sin voltearlo. La versión de React pasa por useLhPopper de forma uniforme (la misma geometría que Dialog / Tooltip, con el mismo volteo por desbordamiento y el mismo repliegue al desplazar).
8. Dialog es puramente controlado
El Dialog de plus mantiene internamente un estado intermedio isVisible y lo sincroniza bidireccionalmente con modelValue mediante watch. La versión de React elimina el estado intermedio: open lo mantiene el exterior y todas las rutas de cierre solo llaman a onClose. La semántica controlada es más pura y desaparece la condición de carrera entre props y estado interno.
9. El ciclo de vida de Dialog se divide en dos grupos de effects
La versión de Vue usa un único callback watch(visible, cb, { immediate: true }) para gestionar abrir y cerrar. La versión de React lo separa en un «open effect» (registro en la pila de capas, bloqueo del desplazamiento, disparo de onOpen) y un «mounted effect» (focus trap, disparo de onOpened / onClosed), porque en React el momento del montaje de la animación de transición y el del apertura son dos ciclos de vida independientes.
10. Corrección de la validación asíncrona de Form
El validateField de plus usa Promise.resolve(!message) y no espera la Promise que devuelve el validador asíncrono, de modo que la validación asíncrona siempre devuelve true. La versión de React añade una comprobación isThenable y aguarda explícitamente a que el validador asíncrono termine antes de decidir:
if (isThenable(message)) return message.then((error) => !error)
Este defecto está registrado en el bugfix-log de la familia; la corrección de la versión de Vue está pendiente de retroportar.
11. Simplificación del control de Tooltip
El Tooltip de plus tiene un interruptor manual que acompaña al control con visible. La versión de React basta con mirar la prop visible: si se pasa visible, es controlado, y el callback onVisibleChange avisa al exterior; ya no se conserva el interruptor intermedio manual.
12. Mejoras del lado React (todavía no en plus)
Las siguientes diferencias son mejoras de la versión de React; al migrar se usan como nuevas capacidades:
- Tag: cerrar implica desmontaje: tras hacer clic en el botón de cerrar, la Tag se desmonta del DOM de inmediato y solo entonces emite
onClose(plus solo emite el evento y es elv-ifexterno quien retira el nodo). - Input: el borrado reescribe el valor: el botón de borrado de
clearableemite tambiénonChange(''), de modo que el estado controlado se sincroniza solo (el@clearde plus solo emite el evento). - Form: validación en change: las reglas con
rule.trigger === 'change'se disparan al propagarse el change del área de contenido (en plus las impulsa el watch de reactive). - Form: el restablecimiento borra los errores:
resetFields()incrementa un número de versión interno; cada FormItem lo observa y vacía su estado de error (en plus se borra solo mediante la cadena reactiva). - Input: mostrar / ocultar contraseña: la prop
showPasswordrenderiza, contype="password", un botón de ojo que conmuta a texto plano; plus / vue2 no tienen esta prop.
13. Adaptaciones específicas de React en los componentes Wave C
- Drawer: la máscara no se puede atravesar con clics por defecto:
maskClosableesfalsepor defecto (elcanDrawerClosede la familia lo rechaza por defecto); elcloseOnClickModalde plus estruepor defecto, de modo que al migrar hay que declararlo explícitamente. El encierro de foco solo se activa cuandolockScrollestá activado (shouldDrawerTrapFocus); si no se bloquea el desplazamiento, se preserva la secuencia de Tab del contenido situado detrás. - Dropdown: el slot pasa a ser prop: el slot
#dropdownde plus pasa a ser, en React, la prop homónimadropdown; los elementos de menú registran su DOM mediante Context, y el roving tabindex lo impulsa el estado de anclajeactiveEl(la versión de Vue recolecta las instancias de los subcomponentes congetCurrentInstance). - Popover / Popconfirm pasan a ser controlados: al igual que Tooltip, pasar
visiblesignifica controlado yonVisibleChangese emite hacia fuera; no existe el interruptormanualde plus.
Tabla rápida de migración de tipos
// Vue: las props se infieren a partir de withDefaults
const props = withDefaults(defineProps<LhButtonProps>(), { type: 'text' })
// React: interfaz + valores por defecto en la deestructuración
function LhButton(props: LhButtonProps) {
const { type = 'text' } = props
}
// Vue: declaración de emits
const emit = defineEmits<{ change: [val: string, e: Event] }>()
// React: callbacks en las props
interface LhXxxProps {
onChange?: (value: string, event: React.ChangeEvent<HTMLInputElement>) => void
}