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é #header | prop header?: ReactNode |
provide(...) / inject(...) | Context.Provider / useContext |
provide doit utiliser computed | La 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 / onUnmounted | Setup / 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
onClosen'est émis qu'ensuite (plus se contente d'émettre l'événement, le retrait du nœud étant assuré par unv-ifexterne). - Réécriture à l'effacement de Input : le bouton d'effacement de
clearableémet simultanémentonChange(''), de sorte que l'état contrôlé se synchronise automatiquement (le@clearde 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
showPasswordrend, lorsquetype="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 :
maskClosablevautfalsepar défaut (lecanDrawerClosede core refuse par défaut), alors quecloseOnClickModalvauttruepar défaut dans plus — il faut donc le déclarer explicitement lors de la migration. L'anneau de focus n'est activé que lorsquelockScrollest 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
#dropdownde plus devient une propdropdownde 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'ancrageactiveEl(la version Vue s'appuie surgetCurrentInstancepour collecter les instances des composants enfants). - Popover / Popconfirm contrôlés : comme pour Tooltip, passer
visiblerend le composant contrôlé etonVisibleChangeest émis vers l'extérieur, sans l'interrupteurmanualde 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
}