Saltar al contenido principal

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 #headerprop header?: ReactNode
provide(...) / inject(...)Context.Provider / useContext
provide debe usar computedel 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 / onUnmountedsetup / 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 el v-if externo quien retira el nodo).
  • Input: el borrado reescribe el valor: el botón de borrado de clearable emite también onChange(''), de modo que el estado controlado se sincroniza solo (el @clear de 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 showPassword renderiza, con type="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: maskClosable es false por defecto (el canDrawerClose de la familia lo rechaza por defecto); el closeOnClickModal de plus es true por defecto, de modo que al migrar hay que declararlo explícitamente. El encierro de foco solo se activa cuando lockScroll está 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 #dropdown de plus pasa a ser, en React, la prop homónima dropdown; los elementos de menú registran su DOM mediante Context, y el roving tabindex lo impulsa el estado de anclaje activeEl (la versión de Vue recolecta las instancias de los subcomponentes con getCurrentInstance).
  • Popover / Popconfirm pasan a ser controlados: al igual que Tooltip, pasar visible significa controlado y onVisibleChange se emite hacia fuera; no existe el interruptor manual de 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
}