Communitygithub.com

ValentinCmbr/claude-skills

Modèles de développement frontend pour React, Next.js, gestion d'état, optimisation des performances et bonnes pratiques UI. À utiliser lors de la création ou la révision de composants React ou Next.js, l'état ou les performances de rendu.

Qu'est-ce que claude-skills ?

Ce skill fournit une collection complète de modèles et bonnes pratiques de développement frontend, spécialement conçus pour React et Next.js. Il couvre la composition de composants, la gestion d'état avec des hooks personnalisés (comme useToggle, useQuery et useDebounce), l'optimisation des performances via la mémoïsation, le fractionnement de code et la virtualisation, ainsi que la gestion de formulaires avec validation. Il inclut également des modèles d'accessibilité et d'animations avec Framer Motion. Le skill s'active automatiquement lorsque l'utilisateur travaille sur des composants, la gestion d'état, la récupération de données, l'optimisation des performances ou l'accessibilité dans React, Next.js ou des applications UI modernes. Il offre des exemples de code pratiques avec des commentaires expliquant pourquoi certaines approches sont meilleures, comme l'utilisation de refs pour stabiliser les callbacks de refetch ou la copie de tableaux avant tri, aidant ainsi les développeurs à écrire un code plus maintenable et performant.

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ValentinCmbr/claude-skills/tree/HEAD/plugins/skills/skills/frontend-patterns

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Modèles de Développement Frontend

Modèles frontend modernes pour React, Next.js et interfaces utilisateur performantes.

Quand l'activer

  • Construire des composants React (composition, props, rendu)
  • Gérer l'état (useState, useReducer, Zustand, Context)
  • Implémenter la récupération de données (SWR, React Query, composants serveur)
  • Optimiser les performances (mémoïsation, virtualisation, fractionnement de code)
  • Travailler avec des formulaires (validation, entrées contrôlées, schémas Zod)
  • Gérer le routage et la navigation côté client
  • Construire des modèles UI accessibles et réactifs

Modèles de Composants

Composition plutôt qu'héritage

// PASS : BON : Composition de composants
interface CardProps {
  children: React.ReactNode
  variant?: 'default' | 'outlined'
}

export function Card({ children, variant = 'default' }: CardProps) {
  return <div className={`card card-${variant}`}>{children}</div>
}

export function CardHeader({ children }: { children: React.ReactNode }) {
  return <div className="card-header">{children}</div>
}

export function CardBody({ children }: { children: React.ReactNode }) {
  return <div className="card-body">{children}</div>
}

// Utilisation
<Card>
  <CardHeader>Titre</CardHeader>
  <CardBody>Contenu</CardBody>
</Card>

Composants Composés

interface TabsContextValue {
  activeTab: string
  setActiveTab: (tab: string) => void
}

const TabsContext = createContext<TabsContextValue | undefined>(undefined)

export function Tabs({ children, defaultTab }: {
  children: React.ReactNode
  defaultTab: string
}) {
  const [activeTab, setActiveTab] = useState(defaultTab)

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  )
}

export function TabList({ children }: { children: React.ReactNode }) {
  return <div className="tab-list">{children}</div>
}

export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
  const context = useContext(TabsContext)
  if (!context) throw new Error('Tab doit être utilisé à l\'intérieur de Tabs')

  return (
    <button
      className={context.activeTab === id ? 'active' : ''}
      onClick={() => context.setActiveTab(id)}
    >
      {children}
    </button>
  )
}

// Utilisation
<Tabs defaultTab="apercu">
  <TabList>
    <Tab id="apercu">Aperçu</Tab>
    <Tab id="details">Détails</Tab>
  </TabList>
</Tabs>

Modèle Render Props

interface DataLoaderProps<T> {
  url: string
  children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
}

export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false))
  }, [url])

  return <>{children(data, loading, error)}</>
}

// Utilisation
<DataLoader<Market[]> url="/api/markets">
  {(markets, loading, error) => {
    if (loading) return <Spinner />
    if (error) return <Error error={error} />
    return <MarketList markets={markets!} />
  }}
</DataLoader>

Modèles de Hooks Personnalisés

Hook de Gestion d'État

export function useToggle(initialValue = false): [boolean, () => void] {
  const [value, setValue] = useState(initialValue)

  const toggle = useCallback(() => {
    setValue(v => !v)
  }, [])

  return [value, toggle]
}

// Utilisation
const [isOpen, toggleOpen] = useToggle()

Hook de Récupération de Données Asynchrone

interface UseQueryOptions<T> {
  onSuccess?: (data: T) => void
  onError?: (error: Error) => void
  enabled?: boolean
}

export function useQuery<T>(
  key: string,
  fetcher: () => Promise<T>,
  options?: UseQueryOptions<T>
) {
  const [data, setData] = useState<T | null>(null)
  const [error, setError] = useState<Error | null>(null)
  const [loading, setLoading] = useState(false)

  // Garder le dernier fetcher/options dans des refs pour que refetch reste
  // stable référentiellement même lorsque les appelants passent des fonctions
  // inline et des littéraux d'objet. Sans cela, chaque rendu crée un nouveau
  // refetch, et l'effet ci-dessous se réexécute après chaque mise à jour d'état -
  // une boucle de récupération infinie.
  const fetcherRef = useRef(fetcher)
  const optionsRef = useRef(options)
  useEffect(() => {
    fetcherRef.current = fetcher
    optionsRef.current = options
  })

  const refetch = useCallback(async () => {
    setLoading(true)
    setError(null)

    try {
      const result = await fetcherRef.current()
      setData(result)
      optionsRef.current?.onSuccess?.(result)
    } catch (err) {
      const error = err as Error
      setError(error)
      optionsRef.current?.onError?.(error)
    } finally {
      setLoading(false)
    }
  }, [])

  const enabled = options?.enabled !== false

  useEffect(() => {
    if (enabled) {
      refetch()
    }
  }, [key, enabled, refetch])

  return { data, error, loading, refetch }
}

// Utilisation
const { data: markets, loading, error, refetch } = useQuery(
  'markets',
  () => fetch('/api/markets').then(r => r.json()),
  {
    onSuccess: data => console.log('Récupéré', data.length, 'markets'),
    onError: err => console.error('Échec :', err)
  }
)

Hook Debounce

export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value)

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)

    return () => clearTimeout(handler)
  }, [value, delay])

  return debouncedValue
}

// Utilisation
const [searchQuery, setSearchQuery] = useState('')
const debouncedQuery = useDebounce(searchQuery, 500)

useEffect(() => {
  if (debouncedQuery) {
    performSearch(debouncedQuery)
  }
}, [debouncedQuery])

Modèles de Gestion d'État

Modèle Context + Reducer

interface State {
  markets: Market[]
  selectedMarket: Market | null
  loading: boolean
}

type Action =
  | { type: 'SET_MARKETS'; payload: Market[] }
  | { type: 'SELECT_MARKET'; payload: Market }
  | { type: 'SET_LOADING'; payload: boolean }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'SET_MARKETS':
      return { ...state, markets: action.payload }
    case 'SELECT_MARKET':
      return { ...state, selectedMarket: action.payload }
    case 'SET_LOADING':
      return { ...state, loading: action.payload }
    default:
      return state
  }
}

const MarketContext = createContext<{
  state: State
  dispatch: Dispatch<Action>
} | undefined>(undefined)

export function MarketProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, {
    markets: [],
    selectedMarket: null,
    loading: false
  })

  return (
    <MarketContext.Provider value={{ state, dispatch }}>
      {children}
    </MarketContext.Provider>
  )
}

export function useMarkets() {
  const context = useContext(MarketContext)
  if (!context) throw new Error('useMarkets doit être utilisé à l\'intérieur de MarketProvider')
  return context
}

Optimisation des Performances

Mémoïsation

// PASS : useMemo pour les calculs coûteux
// Copier avant de trier - Array.prototype.sort modifie sur place
const sortedMarkets = useMemo(() => {
  return [...markets].sort((a, b) => b.volume - a.volume)
}, [markets])

// PASS : useCallback pour les fonctions passées aux enfants
const handleSearch = useCallback((query: string) => {
  setSearchQuery(query)
}, [])

// PASS : React.memo pour les composants purs
export const MarketCard = React.memo<MarketCardProps>(({ market }) => {
  return (
    <div className="market-card">
      <h3>{market.name}</h3>
      <p>{market.description}</p>
    </div>
  )
})

Fractionnement de Code et Chargement Paresseux

import { lazy, Suspense } from 'react'

// PASS : Charger paresseusement les composants lourds
const HeavyChart = lazy(() => import('./HeavyChart'))
const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))

export function Dashboard() {
  return (
    <div>
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart data={data} />
      </Suspense>

      <Suspense fallback={null}>
        <ThreeJsBackground />
      </Suspense>
    </div>
  )
}

Virtualisation pour Longues Listes

import { useVirtualizer } from '@tanstack/react-virtual'

export function VirtualMarketList({ markets }: { markets: Market[] }) {
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: markets.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 100,  // Hauteur de ligne estimée
    overscan: 5  // Éléments supplémentaires à rendre
  })

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative'
        }}
      >
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.index}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualRow.size}px`,
              transform: `translateY(${virtualRow.start}px)`
            }}
          >
            <MarketCard market={markets[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  )
}

Modèles de Gestion de Formulaires

Formulaire Contrôlé avec Validation

interface FormData {
  name: string
  description: string
  endDate: string
}

interface FormErrors {
  name?: string
  description?: string
  endDate?: string
}

export function CreateMarketForm() {
  const [formData, setFormData] = useState<FormData>({
    name: '',
    description: '',
    endDate: ''
  })

  const [errors, setErrors] = useState<FormErrors>({})

  const validate = (): boolean => {
    const newErrors: FormErrors = {}

    if (!formData.name.trim()) {
      newErrors.name = 'Le nom est requis'
    } else if (formData.name.length > 200) {
      newErrors.name = 'Le nom doit faire moins de 200 caractères'
    }

    if (!formData.description.trim()) {
      newErrors.description = 'La description est requise'
    }

    if (!formData.endDate) {
      newErrors.endDate = 'La date de fin est requise'
    }

    setErrors(newErrors)
    return Object.keys(newErrors).length === 0
  }

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()

    if (!validate()) return

    try {
      await createMarket(formData)
      // Gestion du succès
    } catch (error) {
      // Gestion de l'erreur
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.name}
        onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
        placeholder="Nom du marché"
      />
      {errors.name && <span className="error">{errors.name}</span>}

      {/* Autres champs */}

      <button type="submit">Créer le marché</button>
    </form>
  )
}

Modèle de Limite d'Erreur

interface ErrorBoundaryState {
  hasError: boolean
  error: Error | null
}

export class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  ErrorBoundaryState
> {
  state: ErrorBoundaryState = {
    hasError: false,
    error: null
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Limite d\'erreur attrapée :', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>Quelque chose s'est mal passé</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false })}>
            Réessayer
          </button>
        </div>
      )
    }

    return this.props.children
  }
}

// Utilisation
<ErrorBoundary>
  <App />
</ErrorBoundary>

Modèles d'Animation

Animations Framer Motion

import { motion, AnimatePresence } from 'framer-motion'

// PASS : Animations de liste
export function AnimatedMarketList({ markets }: { markets: Market[] }) {
  return (
    <AnimatePresence>
      {markets.map(market => (
        <motion.div
          key={market.id}
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -20 }}
          transition={{ duration: 0.3 }}
        >
          <MarketCard market={market} />
        </motion.div>
      ))}
    </AnimatePresence>
  )
}

// PASS : Animations de modale
export function Modal({ isOpen, onClose, children }: ModalProps) {
  return (
    <AnimatePresence>
      {isOpen && (
        <>
          <motion.div
            className="modal-overlay"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={onClose}
          />
          <motion.div
            className="modal-content"
            initial={{ opacity: 0, scale: 0.9, y: 20 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.9, y: 20 }}
          >
            {children}
          </motion.div>
        </>
      )}
    </AnimatePresence>
  )
}

Modèles d'Accessibilité

Navigation au Clavier

export function Dropdown({ options, onSelect }: DropdownProps) {
  const [isOpen, setIsOpen] = useState(false)
  const [activeIndex, setActiveIndex] = useState(0)

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault()
        setActiveIndex(i => Math.min(i + 1, options.length - 1))
        break
      case 'ArrowUp':
        e.preventDefault()
        setActiveIndex(i => Math.max(i - 1, 0))
        break
      case 'Enter':
        e.preventDefault()
        onSelect(options[activeIndex])
        setIsOpen(false)
        break
      case 'Escape':
        e.preventDefault()
        setIsOpen(false)
        break
    }
  }

  return (
    <div className="dropdown" onKeyDown={handleKeyDown}>
      <button onClick={() => setIsOpen(!isOpen)}>
        Sélectionner une option
      </button>
      {isOpen && (
        <ul className="dropdown-menu">
          {options.map((option, index) => (
            <li
              key={option.value}
              className={index === activeIndex ? 'active' : ''}
              onClick={() => {
                onSelect(option)
                setIsOpen(false)
              }}
            >
              {option.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

Modèles de Composants Serveur Next.js

Composants Serveur

// Ce composant s'exécute sur le serveur - aucune interactivité côté client
// Les composants serveur peuvent directement attendre des données async
// sans avoir besoin de hooks ni d'états de chargement

export default async function MarketPage() {
  const markets = await getMarkets()

  return (
    <div>
      <h1>Marchés</h1>
      <MarketList markets={markets} />
    </div>
  )
}

Composants Serveur avec Rendu Partiel

import { Suspense } from 'react'

// Le composant parent est un composant serveur
export default function DashboardPage() {
  return (
    <div>
      {/* Contenu statique rendu immédiatement */}
      <h1>Tableau de bord</h1>
      <p>Bienvenue sur votre tableau de bord</p>

      {/* Contenu dynamique avec streaming */}
      <Suspense fallback={<div>Chargement des marchés...</div>}>
        <MarketList />
      </Suspense>

      <Suspense fallback={<div>Chargement des notifications...</div>}>
        <NotificationList />
      </Suspense>
    </div>
  )
}

// Composant serveur avec données asynchrones
async function MarketList() {
  const markets = await fetchMarkets()
  return (
    <ul>
      {markets.map(market => (
        <li key={market.id}>
          {market.name} - {market.price}
        </li>
      ))}
    </ul>
  )
}

async function NotificationList() {
  const notifications = await fetchNotifications()
  return (
    <ul>
      {notifications.map(notification => (
        <li key={notification.id} className="notification">
          {notification.message}
        </li>
      ))}
    </ul>
  )
}

Modèles de Composants Côté Client Next.js

Utilisez 'use client' pour l'Interactivité

'use client'

import { useState } from 'react'

export function SearchMarkets() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<Market[]>([])

  const handleSearch = async () => {
    const data = await searchMarkets(query)
    setResults(data)
  }

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Rechercher des marchés..."
      />
      <button onClick={handleSearch}>Rechercher</button>

      <ul>
        {results.map(market => (
          <li key={market.id}>{market.name}</li>
        ))}
      </ul>
    </div>
  )
}

Routage et Navigation

Utilisation de next/navigation

'use client'

import { useRouter, usePathname, useSearchParams } from 'next/navigation'

export function Navigation() {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()

  const createQueryString = (name: string, value: string) => {
    const params = new URLSearchParams(searchParams.toString())
    params.set(name, value)
    return params.toString()
  }

  return (
    <div>
      <p>Chemin actuel : {pathname}</p>
      <button onClick={() => router.push('/markets')}>
        Voir les marchés
      </button>
      <button
        onClick={() => {
          router.push('/markets?' + createQueryString('sort', 'volume'))
        }}
      >
        Trier par volume
      </button>
    </div>
  )
}

Tests

Test de Hook Personnalisé

import { renderHook, act } from '@testing-library/react'
import { useToggle } from './useToggle'

describe('useToggle', () => {
  it('devrait commencer par la valeur par défaut', () => {
    const { result } = renderHook(() => useToggle())
    expect(result.current[0]).toBe(false)
  })

  it('devrait basculer la valeur', () => {
    const { result } = renderHook(() => useToggle())

    act(() => {
      result.current[1]()
    })

    expect(result.current[0]).toBe(true)

    act(() => {
      result.current[1]()
    })

    expect(result.current[0]).toBe(false)
  })
})

Test de Composant avec Rendu

import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { CreateMarketForm } from './CreateMarketForm'

describe('CreateMarketForm', () => {
  it('devrait afficher les erreurs de validation', async () => {
    render(<CreateMarketForm />)

    const submitButton = screen.getByText('Créer le marché')
    fireEvent.click(submitButton)

    expect(await screen.findByText('Le nom est requis')).toBeInTheDocument()
    expect(await screen.findByText('La description est requise')).toBeInTheDocument()
    expect(await screen.findByText('La date de fin est requise')).toBeInTheDocument()
  })

  it('devrait soumettre avec des données valides', async () => {
    const user = userEvent.setup()

    render(<CreateMarketForm />)

    const nameInput = screen.getByPlaceholderText('Nom du marché')
    await user.type(nameInput, 'Mon Marché')

    // Remplir les autres champs et soumettre...
  })
})

Skills associés