Web Component Design
Construisez des composants d'interface utilisateur réutilisables et maintenables en utilisant des frameworks modernes avec des motifs de composition propres et des approches de stylisation.
Quand utiliser cette compétence
- Concevoir des bibliothèques de composants réutilisables ou des systèmes de conception
- Implémenter des motifs complexes de composition de composants
- Choisir et appliquer des solutions CSS-in-JS
- Construire des composants d'interface utilisateur accessibles et réactifs
- Créer des API de composants cohérentes dans une base de code
- Refactoriser des composants legacy en motifs modernes
- Implémenter des composants composés ou render props
Concepts fondamentaux
1. Motifs de composition de composants
Composants composés : Des composants liés qui travaillent ensemble
// Usage
<Select value={value} onChange={setValue}>
<Select.Trigger>Choose option</Select.Trigger>
<Select.Options>
<Select.Option value="a">Option A</Select.Option>
<Select.Option value="b">Option B</Select.Option>
</Select.Options>
</Select>
Render Props : Déléguer le rendu au composant parent
<DataFetcher url="/api/users">
{({ data, loading, error }) =>
loading ? <Spinner /> : <UserList users={data} />
}
</DataFetcher>
Slots (Vue/Svelte) : Points d'injection de contenu nommés
<template>
<Card>
<template #header>Title</template>
<template #content>Body text</template>
<template #footer><Button>Action</Button></template>
</Card>
</template>
2. Approches CSS-in-JS
| Solution | Approche | Idéal pour |
|---|---|---|
| Tailwind CSS | Classes utilitaires | Prototypage rapide, systèmes de conception |
| CSS Modules | Fichiers CSS scoped | CSS existant, adoption progressive |
| styled-components | Template literals | React, stylisation dynamique |
| Emotion | Styles objet/template | Flexible, compatible SSR |
| Vanilla Extract | Zéro runtime | Applications critiques en performance |
3. Conception d'API de composants
interface ButtonProps {
variant?: "primary" | "secondary" | "ghost";
size?: "sm" | "md" | "lg";
isLoading?: boolean;
isDisabled?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
children: React.ReactNode;
onClick?: () => void;
}
Principes :
- Utiliser des noms de props sémantiques (
isLoadingau lieu deloading) - Fournir des valeurs par défaut judicieuses
- Supporter la composition via
children - Permettre les surcharges de style via
classNameoustyle
Démarrage rapide : Composant React avec Tailwind
import { forwardRef, type ComponentPropsWithoutRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
primary: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200",
ghost: "hover:bg-gray-100 hover:text-gray-900",
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
},
);
interface ButtonProps
extends
ComponentPropsWithoutRef<"button">,
VariantProps<typeof buttonVariants> {
isLoading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, isLoading, children, ...props }, ref) => (
<button
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
disabled={isLoading || props.disabled}
{...props}
>
{isLoading && <Spinner className="mr-2 h-4 w-4" />}
{children}
</button>
),
);
Button.displayName = "Button";
Motifs par framework
React : Composants composés
import { createContext, useContext, useState, type ReactNode } from "react";
interface AccordionContextValue {
openItems: Set<string>;
toggle: (id: string) => void;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordion() {
const context = useContext(AccordionContext);
if (!context) throw new Error("Must be used within Accordion");
return context;
}
export function Accordion({ children }: { children: ReactNode }) {
const [openItems, setOpenItems] = useState<Set<string>>(new Set());
const toggle = (id: string) => {
setOpenItems((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
return (
<AccordionContext.Provider value={{ openItems, toggle }}>
<div className="divide-y">{children}</div>
</AccordionContext.Provider>
);
}
Accordion.Item = function AccordionItem({
id,
title,
children,
}: {
id: string;
title: string;
children: ReactNode;
}) {
const { openItems, toggle } = useAccordion();
const isOpen = openItems.has(id);
return (
<div>
<button onClick={() => toggle(id)} className="w-full text-left py-3">
{title}
</button>
{isOpen && <div className="pb-3">{children}</div>}
</div>
);
};
Vue 3 : Composables
<script setup lang="ts">
import { ref, computed, provide, inject, type InjectionKey } from "vue";
interface TabsContext {
activeTab: Ref<string>;
setActive: (id: string) => void;
}
const TabsKey: InjectionKey<TabsContext> = Symbol("tabs");
// Parent component
const activeTab = ref("tab-1");
provide(TabsKey, {
activeTab,
setActive: (id: string) => {
activeTab.value = id;
},
});
// Child component usage
const tabs = inject(TabsKey);
const isActive = computed(() => tabs?.activeTab.value === props.id);
</script>
Svelte 5 : Runes
<script lang="ts">
interface Props {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
onclick?: () => void;
children: import('svelte').Snippet;
}
let { variant = 'primary', size = 'md', onclick, children }: Props = $props();
const classes = $derived(
`btn btn-${variant} btn-${size}`
);
</script>
<button class={classes} {onclick}>
{@render children()}
</button>
Bonnes pratiques
- Responsabilité unique : Chaque composant fait bien une seule chose
- Prévention du prop drilling : Utiliser le contexte pour les données imbriquées profondément
- Accessible par défaut : Inclure les attributs ARIA et le support clavier
- Contrôlé vs non contrôlé : Supporter les deux motifs quand approprié
- Forward Refs : Permettre l'accès parent aux nœuds DOM
- Mémoïsation : Utiliser
React.memo,useMemopour les rendus coûteux - Error Boundaries : Envelopper les composants qui pourraient échouer
Problèmes courants
- Explosion de props : Trop de props - considérer la composition à la place
- Conflits de styles : Utiliser les styles scoped ou CSS Modules
- Cascades de re-render : Profiler avec React DevTools, mémoïser appropriément
- Lacunes d'accessibilité : Tester avec des lecteurs d'écran et la navigation au clavier
- Taille du bundle : Tree-shake les variantes de composants inutilisées