Перейти к основному содержимому

Руководство по миграции

Эта страница рассказывает о двух вещах: общем отображении понятий Vue (lh-ui-plus / lh-ui-vue2) → React и сознательных отличиях этой библиотеки от Vue-версий.

Общая таблица соответствий​

Vue (lh-ui-plus)React (@lh-ui/react)
defineProps<T>()interface LhXxxProps + деструктурированные значения по умолчанию
withDefaults(...)Деструктурированные значения по умолчанию (без 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
Именованный слот #headerprop header?: ReactNode
provide(...) / inject(...)Context.Provider / useContext
provide должен быть computedЗначение Context должно быть в useMemo
computed(() => ...)useMemo(() => ..., [deps])
watch(src, cb)useEffect(() => cb(), [src])
watch(src, cb, { immediate: true })useEffect (эффекты и так выполняются один раз немедленно)
onMounted / onUnmountedsetup / cleanup эффекта useEffect
<Transition name="lh-x-fade">useTransition(visible, duration) управляет теми же именами классов
<Teleport to="body">createPortal(..., document.body)
ref="<name>"useRef<HTMLDivElement>(null)
nextTick()flushSync() или эффект (обычно не требуется)
app.use(LhUI)Ничего — именованные импорты по требованию
<LhXxx v-bind="obj"><LhXxx {...obj}>

Контролируемый и неконтролируемый режимы​

Vue-овские modelValue / update:modelValue в React распадаются на два набора props:

<!-- Vue -->
<LhInput v-model="text" />
// React: контролируемый
<LhInput value={text} onChange={(val) => setText(val)} />

// React: неконтролируемый
<LhInput defaultValue="Начальное значение" />

Смешивание (одновременная передача value и defaultValue) записывает предупреждение в режиме разработки — это соответствует семантике нативных форм React.

Коллбэки событий​

Все props событий используют единое именование onXxx, а сигнатура коллбэка единообразно двухаргументная (value, event):

<!-- Vue -->
<LhSelect @change="onChange" />
// React
<LhSelect onChange={(value, option) => console.log(value, option)} />

Переходы и анимация​

Vue-овский <Transition> декларативен; версия React управляет тем же набором имён классов с помощью собственного хука useTransition:

import { useTransition } from '@lh-ui/react'

const { mounted, phase } = useTransition(visible, 300)
// Класс: `lh-dialog-fade-${phase}` — phase ∈ enter-from / enter-active / enter-to

Имена классов в точности совпадают со SCSS из share, поэтому визуально результат покадрово идентичен Vue-версии.

Оверлеи и портал​

Vue-овский <Teleport to="body"> соответствует React-овскому createPortal; Dialog / выпадающий список Select / Tooltip монтируются в document.body. Геометрия позиционирования вычисляется через useLhPopper (с использованием общего с Vue-версией @lh-ui/share/core/shared/popper).

Императивные ref​

Vue-овский defineExpose соответствует forwardRef + useImperativeHandle:

<!-- Vue -->
<LhForm ref="formRef" />
// React
const formRef = useRef<LhFormInstance>(null)
<LhForm ref={formRef} />
<formRef.current?.validate() />

Глобальная настройка​

<!-- Vue -->
<LhConfigProvider :size="size">
<App />
</LhConfigProvider>
// React
<LhConfigProvider size={size}>
<App />
</LhConfigProvider>

Отличия реализации от Vue-версии​

Перечисленные ниже отличия — сознательные корректировки под React-форму; при миграции обращайте на них внимание. Каждое зарегистрировано в репозитории.

1. Импорт стилей по требованию​

В отличие от lh-ui-plus (полное внедрение), эта библиотека выровнена с lh-ui-vue2: вход пакета не внедряет стили; пользователь явно импортирует theme.css + style/<name>.

2. Сборочная цепочка как у vue2​

Vite lib mode + preserveModules + зеркало tsc, напрямую выдающее d.ts (plus использует rollup со сканированием нескольких точек входа). preserveModules естественно поддерживает импорт по требованию, в tsx нет виртуальных модулей SFC, а генерация d.ts через tsc — простейший вариант.

3. Запасное значение на стороне потребителя ConfigProvider​

Vue-версия использует inject(..., null) и каждый компонент проверяет на null. Потребляющий хук React-версии возвращает запасной объект со всеми полями undefined, и компонентам не нужны null-проверки — это распространённый приём в React, поскольку useContext не позволяет задать значение по умолчанию в точке вызова.

4. Различия монтирования notification / loading​

Все три — императивные сервисы, но монтируются по-разному: message / notification разделяют контейнер-одиночку (ленивый createRoot + канал действий), а loading нужно монтировать в произвольный целевой элемент (полноэкранный или локальный) через per-instance createRoot, где каждый экземпляр держит собственный root и дескрипторы восстановления overflow / inert.

API notification объектного стиля (notification.success({ title, content }), в духе Notification(options) из plus) также принимает строковое сокращение (рендерится только строка заголовка). FLIP-анимация освобождения места не реализована: класс -move, который даёт transition-group во Vue, не имеет React-эквивалента, поэтому родственные карточки мгновенно смещаются при изменении очереди.

5. Двухаргументный onChange у Input​

В Vue-версии @input / @change разделены; в React-версии они объединены в onChange(value, event) (плюс onInput, прокидывающий нативное событие ввода).

6. autosize у Input выполняется компонентом​

Vue-версия использует атрибут data-autosize, чтобы директива считала высоту на уровне DOM. React-версия в useEffect читает scrollHeight и записывает textarea.style.height (в пределах minRows / maxRows), избегая лишнего механизма директив.

7. Позиционирование Select через useLhPopper​

Некоторые Vue-версии Select используют написанное вручную fixed-позиционирование без переворота. React-версия единообразно проходит через useLhPopper (та же геометрия, что у Dialog / Tooltip, тот же переворот при переполнении и свёртывание при прокрутке).

8. Dialog полностью контролируемый​

Dialog из plus хранит внутреннее промежуточное состояние isVisible и синхронизирует modelValue через watch. React-версия убирает промежуточное состояние: open принадлежит внешней стороне, а все пути закрытия лишь вызывают onClose. Контролируемая семантика чище, гонка между props и внутренним состоянием устранена.

9. Жизненный цикл Dialog разбит на две группы эффектов​

Vue-версия использует один коллбэк watch(visible, cb, { immediate: true }) и для открытия, и для закрытия. React-версия разбивает его на «open effect» (регистрация в стеке оверлеев, блокировка прокрутки, вызов onOpen) и «mounted effect» (focus trap, вызовы onOpened / onClosed), поскольку момент монтирования перехода и момент открытия в React — два независимых жизненных цикла.

10. Исправление асинхронной валидации Form​

validateField из plus использует Promise.resolve(!message) и никогда не дожидается Promise, возвращаемого асинхронным validator-ом — асинхронная валидация всегда разрешается true. React-версия добавляет проверку isThenable и явно ждёт завершения асинхронных валидаторов перед принятием решения:

if (isThenable(message)) return message.then((error) => !error)

Этот дефект зарегистрирован в журнале исправлений семейства; исправление для Vue-версии ещё предстоит внести.

11. Упрощение управляемого режима Tooltip​

У Tooltip из plus есть переключатель manual, работающий в паре с контролируемым visible. React-версия просто читает prop visible: передали visible — значит контролируемый, onVisibleChange уведомляет внешнюю сторону, промежуточный переключатель manual убран.

12. Улучшения на стороне React (которых пока нет в plus)​

Перечисленные отличия — улучшения React-версии; при миграции просто используйте новые возможности:

  • Размонтирование Tag при закрытии: после клика по кнопке закрытия Tag немедленно размонтируется из DOM, и лишь затем выбрасывает onClose (plus лишь выбрасывает событие, а узел удаляет внешняя сторона через v-if).
  • Обратная запись при очистке Input: кнопка очистки clearable также выбрасывает onChange(''), и контролируемое состояние синхронизируется автоматически (@clear в plus лишь выбрасывает событие).
  • Валидация Form по change: правила с rule.trigger === 'change' срабатывают при всплытии change из области содержимого (в plus они запускаются reactive-наблюдателем).
  • Сброс Form очищает ошибки: resetFields() инкрементирует внутренний номер версии, а каждый FormItem наблюдает за ним и очищает состояние ошибки (в plus очистка происходит по reactive-цепочке).
  • Переключение видимости пароля в Input: prop showPassword при type="password" рендерит кнопку с иконкой глаза для переключения на открытый текст; в plus / vue2 такого prop нет.

13. React-спецификации компонентов Wave C​

  • Маска Drawer по умолчанию не закрывается по клику: maskClosable по умолчанию false (функция canDrawerClose из core по умолчанию запрещает), а closeOnClickModal в plus по умолчанию true — при миграции объявляйте это явно. Перехват фокуса включается только при включённом lockScroll (shouldDrawerTrapFocus); без блокировки прокрутки порядок Tab для содержимого позади сохраняется.
  • Слот Dropdown стал prop: слот #dropdown из plus на стороне React превратился в одноимённый prop dropdown; пункты меню регистрируют свой DOM через Context, а roving tabindex управляется якорным состоянием activeEl (Vue-версия собирает дочерние экземпляры компонентов через getCurrentInstance).
  • Popover / Popconfirm переходят в контролируемый режим: как и Tooltip — передали visible, значит контролируемый, onVisibleChange уведомляет внешнюю сторону; переключателя manual из plus нет.

Шпаргалка по миграции типов​

// Vue: props выводятся из withDefaults
const props = withDefaults(defineProps<LhButtonProps>(), { type: 'text' })

// React: интерфейс + деструктурированные значения по умолчанию
function LhButton(props: LhButtonProps) {
const { type = 'text' } = props
}
// Vue: объявление emit
const emit = defineEmits<{ change: [val: string, e: Event] }>()

// React: коллбэк в props
interface LhXxxProps {
onChange?: (value: string, event: React.ChangeEvent<HTMLInputElement>) => void
}