Aller au contenu principal

Guide de migration

Cette page traite deux sujets : la table de correspondance des concepts généraux entre Vue (lh-ui-plus / lh-ui-vue2) et React, ainsi que les différences délibérées de cette bibliothèque par rapport à la version Vue.

Table de correspondance des concepts​

Vue (lh-ui-plus)React (@lh-ui/react)
defineProps<T>()interface LhXxxProps + paramètres par défaut déstructurés
withDefaults(...)Valeurs par défaut déstructurées (sans 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 nommé #headerprop header?: ReactNode
provide(...) / inject(...)Context.Provider / useContext
provide doit utiliser computedLa valeur du Context doit utiliser useMemo
computed(() => ...)useMemo(() => ..., [deps])
watch(src, cb)useEffect(() => cb(), [src])
watch(src, cb, { immediate: true })useEffect (un effect s'exécute toujours une fois immédiatement)
onMounted / onUnmountedSetup / cleanup du useEffect
<Transition name="lh-x-fade">useTransition(visible, duration) pilote les mêmes noms de classe
<Teleport to="body">createPortal(..., document.body)
ref="<name>"useRef<HTMLDivElement>(null)
nextTick()flushSync() ou un effect (généralement inutile)
app.use(LhUI)Rien — les exports nommés s'importent à la demande
<LhXxx v-bind="obj"><LhXxx {...obj}>

Contrôlé et non contrôlé​

Le couple modelValue / update:modelValue de Vue est dédoublé en deux séries de props côté React :

<!-- Vue -->
<LhInput v-model="text" />
// React : contrôlé
<LhInput value={text} onChange={(val) => setText(val)} />

// React : non contrôlé
<LhInput defaultValue="Valeur initiale" />

L'usage mixte (passer à la fois value et defaultValue) émet un avertissement en développement, conformément à la sémantique des formulaires natifs React.

Callbacks d'événement​

Toutes les props d'événement suivent la convention onXxx, et la signature des callbacks adopte uniformément deux paramètres (value, event) :

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

Transitions​

Le <Transition> de Vue est déclaratif ; la version React utilise le hook interne useTransition pour piloter le même ensemble de noms de classe :

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

const { mounted, phase } = useTransition(visible, 300)
// Nom de classe : `lh-dialog-fade-${phase}` — phase ∈ enter-from / enter-active / enter-to

Les noms de classe sont strictement identiques à ceux du SCSS de share, et le rendu visuel est identique image par image à la version Vue.

Calques et téléportation​

Le <Teleport to="body"> de Vue correspond au createPortal de React : Dialog, la liste déroulante de Select et Tooltip sont tous montés sur document.body. La géométrie de positionnement est calculée par useLhPopper (@lh-ui/share/core/shared/popper est partagé avec la version Vue).

ref impératif​

Le defineExpose de Vue correspond à forwardRef + useImperativeHandle :

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

Configuration globale​

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

Différences d'implémentation avec la version Vue​

Les différences suivantes sont des ajustements délibérés liés à la forme React ; elles appellent une attention lors de la migration. Chacune est consignée dans le dépôt.

1. Injection de styles à la demande​

Contrairement à lh-ui-plus (injection globale), cette bibliothèque s'aligne sur lh-ui-vue2 : l'entrée du paquet n'injecte aucun style ; c'est à l'utilisateur d'importer explicitement theme.css + style/<name>.

2. Chaîne de construction identique à vue2​

Mode librairie vite + preserveModules + tsc produisant les d.ts en miroir direct (plus utilise rollup avec un scan multi-entrées). preserveModules soutient naturellement l'import à la demande, les fichiers tsx ne nécessitent pas de module virtuel SFC, et la sortie d.ts directe par tsc est la plus simple.

3. Valeur de repli côté consommation du ConfigProvider​

La version Vue utilise inject(..., null) et laisse chaque composant tester la nullité. Les hooks de consommation de la version React retournent un objet de repli dont tous les champs sont undefined, si bien que les composants n'ont pas besoin de tester la nullité — une pratique courante avec React Context, car useContext ne permet pas de fournir une valeur par défaut au point d'appel.

4. Différence de montage entre notification et loading​

Les trois sont des services impératifs, mais leur mode de montage diffère : message / notification partagent un conteneur singleton (createRoot paresseux + canal d'actions), tandis que loading doit être monté sur un élément cible arbitraire (plein écran ou local), via un createRoot par instance, chaque instance détenant son propre root et ses propres descripteurs de restauration overflow / inert.

L'API de notification est de forme objet (notification.success({ title, content }), alignée sur Notification(options) de plus), et accepte aussi un raccourci sous forme de chaîne (uniquement le rendu de la ligne de titre). L'animation de déplacement FLIP n'est pas implémentée : la classe -move fournie par le transition-group de Vue n'a pas d'équivalent natif côté React, et les cartes adjacentes se repositionnent instantanément lors des ajouts et retraits dans la file.

5. onChange de Input à deux paramètres​

Dans la version Vue, @input / @change sont séparés ; la version React les unifie en onChange(value, event) (avec en complément onInput, qui transmet l'événement natif de saisie).

6. L'autosize de Input manipule le DOM directement​

La version Vue calcule la hauteur au niveau du DOM via l'attribut data-autosize et une directive. La version React lit scrollHeight directement dans useEffect et écrit textarea.style.height (contraint par minRows / maxRows), afin d'éviter d'introduire un mécanisme de directive supplémentaire.

7. Le positionnement de Select passe par useLhPopper​

Certaines versions Vue du Select implémentent un positionnement fixed écrit à la main, sans retournement. La version React passe systématiquement par useLhPopper (même géométrie que Dialog / Tooltip, même retournement sur dépassement et même refermeture au défilement).

8. Dialog purement contrôlé​

Le Dialog de plus détient en interne un état intermédiaire isVisible, synchronisé en double liaison avec modelValue via watch. La version React supprime cet état intermédiaire : open est détenu par l'extérieur, et tous les chemins de fermeture ne font qu'appeler onClose. La sémantique contrôlée est plus pure, et la concurrence entre props et état interne est éliminée.

9. Le cycle de vie de Dialog est éclaté en deux groupes d'effects​

La version Vue utilise un unique callback watch(visible, cb, { immediate: true }) pour gérer l'ouverture et la fermeture. La version React le sépare en « open effect » (inscription à la pile de couches, verrouillage du défilement, déclenchement de onOpen) et « mounted effect » (focus trap, déclenchement de onOpened / onClosed), car la chronologie de montage de la transition et la chronologie d'ouverture sont deux cycles de vie indépendants en React.

10. Correction de la validation asynchrone de Form​

Le validateField de plus utilise Promise.resolve(!message) et n'attend pas la Promise renvoyée par un validateur asynchrone — la validation asynchrone renvoie toujours true. La version React ajoute un test isThenable et attend explicitement la fin du validateur asynchrone avant de conclure :

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

Ce défaut est consigné dans le registre des corrections de bugs de la famille ; la correction côté Vue reste à réinjecter.

11. Simplification du contrôle du Tooltip​

Le Tooltip de plus dispose d'un interrupteur manual associé au visible contrôlé. La version React se contente de la prop visible : tant que visible est passée, le composant est contrôlé, et le callback onVisibleChange en informe la logique externe ; l'interrupteur intermédiaire manual disparaît.

12. Améliorations spécifiques à React (inexistantes dans plus)​

Les différences suivantes sont des améliorations de la version React, à adopter comme de nouvelles capacités lors de la migration :

  • Démontage du Tag à la fermeture : après un clic sur le bouton de fermeture, le Tag est immédiatement démonté du DOM, et onClose n'est émis qu'ensuite (plus se contente d'émettre l'événement, le retrait du nœud étant assuré par un v-if externe).
  • Réécriture à l'effacement de Input : le bouton d'effacement de clearable émet simultanément onChange(''), de sorte que l'état contrôlé se synchronise automatiquement (le @clear de plus ne fait qu'émettre l'événement).
  • Validation au change de Form : les règles dont rule.trigger === 'change' sont déclenchées par la remontée du change de la zone de contenu (plus s'appuie sur le watch de la reactive).
  • Réinitialisation de Form effaçant les erreurs : resetFields() incrémente un numéro de version interne, et chaque FormItem efface son état d'erreur en réaction (plus s'appuie sur la chaîne reactive pour effacer automatiquement).
  • Affichage du mot de passe Input : la prop showPassword rend, lorsque type="password", un bouton en forme d'œil basculant l'affichage en clair ; plus / vue2 n'ont pas cette prop.

13. Spécialisations React des composants Wave C​

  • Le masque du Drawer n'est pas cliquable par défaut : maskClosable vaut false par défaut (le canDrawerClose de core refuse par défaut), alors que closeOnClickModal vaut true par défaut dans plus — il faut donc le déclarer explicitement lors de la migration. L'anneau de focus n'est activé que lorsque lockScroll est ouvert (shouldDrawerTrapFocus) ; sans verrouillage du défilement, la séquence Tab du contenu situé en arrière-plan est préservée.
  • Le slot de Dropdown devient une prop : le slot #dropdown de plus devient une prop dropdown de même nom côté React ; les éléments de menu s'enregistrent auprès du DOM via Context, et le roving tabindex est piloté par l'état d'ancrage activeEl (la version Vue s'appuie sur getCurrentInstance pour collecter les instances des composants enfants).
  • Popover / Popconfirm contrôlés : comme pour Tooltip, passer visible rend le composant contrôlé et onVisibleChange est émis vers l'extérieur, sans l'interrupteur manual de plus.

Aide-mémoire de migration des types​

// Vue : les props sont inférées depuis withDefaults
const props = withDefaults(defineProps<LhButtonProps>(), { type: 'text' })

// React : interface + valeurs par défaut déstructurées
function LhButton(props: LhButtonProps) {
const { type = 'text' } = props
}
// Vue : déclaration des emits
const emit = defineEmits<{ change: [val: string, e: Event] }>()

// React : callback dans les props
interface LhXxxProps {
onChange?: (value: string, event: React.ChangeEvent<HTMLInputElement>) => void
}