React - Hooks Avancés
Ces hooks sont moins utilisés au quotidien, mais quand tu en as besoin, ils sont indispensables ! Performance, layout, identifiants uniques... tout y est.
useLayoutEffect - Synchrone et Bloquant
Différence avec useEffect
import { useState, useEffect, useLayoutEffect, useRef } from 'react'
function LayoutEffectDemo() {
const [count, setCount] = useState(0)
const [height, setHeight] = useState(0)
const divRef = useRef()
// ⚠️ useEffect - S'exécute APRÈS le paint (asynchrone)
useEffect(() => {
console.log('📐 useEffect - APRÈS paint')
// Peut causer un "flash" visuel si on modifie le DOM
})
// ✅ useLayoutEffect - S'exécute AVANT le paint (synchrone)
useLayoutEffect(() => {
console.log('📏 useLayoutEffect - AVANT paint')
if (divRef.current) {
const rect = divRef.current.getBoundingClientRect()
setHeight(rect.height)
}
}) // Bloque le paint jusqu'à ce que ce code soit exécuté !
return (
<div>
<h3>useLayoutEffect Demo</h3>
<div
ref={divRef}
style={{
padding: '20px',
border: '2px solid #007bff',
backgroundColor: '#f8f9fa',
// La hauteur change dynamiquement
fontSize: count > 5 ? '24px' : '16px'
}}
>
<p>Count: {count}</p>
<p>Ce div fait {height}px de haut</p>
<p>Contenu qui peut changer de taille...</p>
</div>
<button onClick={() => setCount(c => c + 1)}>
Incrémenter
</button>
</div>
)
}
Cas d'Usage : Éviter le Flash
function TooltipWithLayoutEffect() {
const [isVisible, setIsVisible] = useState(false)
const [position, setPosition] = useState({ top: 0, left: 0 })
const tooltipRef = useRef()
const triggerRef = useRef()
// ✅ Calculer la position AVANT le paint pour éviter le flash
useLayoutEffect(() => {
if (isVisible && tooltipRef.current && triggerRef.current) {
const triggerRect = triggerRef.current.getBoundingClientRect()
const tooltipRect = tooltipRef.current.getBoundingClientRect()
// Calculer la position optimale
let top = triggerRect.bottom + 8
let left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2)
// Vérifier les débordements d'écran
if (left + tooltipRect.width > window.innerWidth) {
left = window.innerWidth - tooltipRect.width - 8
}
if (left < 8) {
left = 8
}
if (top + tooltipRect.height > window.innerHeight) {
top = triggerRect.top - tooltipRect.height - 8
}
setPosition({ top, left })
}
}, [isVisible])
return (
<div>
<h3>Tooltip avec useLayoutEffect</h3>
<button
ref={triggerRef}
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
style={{ margin: '100px' }}
>
Hover me for tooltip
</button>
{isVisible && (
<div
ref={tooltipRef}
style={{
position: 'fixed',
top: position.top,
left: position.left,
background: '#333',
color: 'white',
padding: '8px 12px',
borderRadius: '4px',
fontSize: '14px',
zIndex: 1000,
// Sans useLayoutEffect, ce tooltip "flasherait"
// à une position incorrecte puis se repositionnerait
}}
>
Je suis un tooltip bien positionné !
</div>
)}
</div>
)
}
useId - Identifiants Uniques
Problème des IDs Statiques
// ❌ PROBLÈME - IDs en dur causent des conflits
function BadForm() {
return (
<div>
<label htmlFor="name">Nom:</label>
<input id="name" type="text" />
<label htmlFor="email">Email:</label>
<input id="email" type="email" />
</div>
)
}
// Si ce composant est utilisé plusieurs fois sur la même page :
function App() {
return (
<div>
<BadForm /> {/* IDs : name, email */}
<BadForm /> {/* IDs : name, email ← CONFLIT ! */}
</div>
)
}
Solution avec useId
function GoodForm() {
const nameId = useId()
const emailId = useId()
const descriptionId = useId()
return (
<div>
<h4>Formulaire avec IDs uniques</h4>
<div>
<label htmlFor={nameId}>Nom:</label>
<input id={nameId} type="text" />
</div>
<div>
<label htmlFor={emailId}>Email:</label>
<input id={emailId} type="email" />
</div>
<div>
<label htmlFor={descriptionId}>Description:</label>
<textarea id={descriptionId} rows={3} />
</div>
</div>
)
}
// Maintenant on peut utiliser le composant plusieurs fois !
function App() {
return (
<div>
<GoodForm /> {/* IDs : :r1:, :r2:, :r3: */}
<GoodForm /> {/* IDs : :r4:, :r5:, :r6: */}
<GoodForm /> {/* IDs : :r7:, :r8:, :r9: */}
</div>
)
}
Patterns Avancés avec useId
function FormFieldWithId({ label, type = 'text', ...props }) {
const id = useId()
return (
<div style={{ marginBottom: '15px' }}>
<label htmlFor={id} style={{ display: 'block', marginBottom: '5px' }}>
{label}
</label>
<input id={id} type={type} {...props} />
</div>
)
}
function RadioGroupWithId({ name, options, value, onChange }) {
const groupId = useId()
return (
<fieldset>
<legend>Choisir une option</legend>
{options.map((option, index) => {
const optionId = `${groupId}-${index}`
return (
<div key={option.value} style={{ marginBottom: '8px' }}>
<input
type="radio"
id={optionId}
name={name}
value={option.value}
checked={value === option.value}
onChange={onChange}
/>
<label htmlFor={optionId} style={{ marginLeft: '8px' }}>
{option.label}
</label>
</div>
)
})}
</fieldset>
)
}
function ComplexForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
category: '',
newsletter: false
})
const handleInputChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
const categoryOptions = [
{ value: 'dev', label: 'Développeur' },
{ value: 'design', label: 'Designer' },
{ value: 'pm', label: 'Product Manager' }
]
return (
<form>
<h3>Formulaire Avancé</h3>
<FormFieldWithId
label="Nom complet"
value={formData.name}
onChange={e => handleInputChange('name', e.target.value)}
placeholder="Votre nom..."
/>
<FormFieldWithId
label="Email"
type="email"
value={formData.email}
onChange={e => handleInputChange('email', e.target.value)}
placeholder="votre@email.com"
/>
<RadioGroupWithId
name="category"
options={categoryOptions}
value={formData.category}
onChange={e => handleInputChange('category', e.target.value)}
/>
<CheckboxWithId
label="S'abonner à la newsletter"
checked={formData.newsletter}
onChange={e => handleInputChange('newsletter', e.target.checked)}
/>
<button type="submit">Envoyer</button>
<pre style={{ marginTop: '20px', background: '#f8f9fa', padding: '10px' }}>
{JSON.stringify(formData, null, 2)}
</pre>
</form>
)
}
function CheckboxWithId({ label, checked, onChange }) {
const id = useId()
return (
<div style={{ marginBottom: '15px' }}>
<input
type="checkbox"
id={id}
checked={checked}
onChange={onChange}
/>
<label htmlFor={id} style={{ marginLeft: '8px' }}>
{label}
</label>
</div>
)
}
useTransition et useDeferredValue - Performance
Priorités de Mise à Jour
import { useState, useTransition, useDeferredValue, useMemo } from 'react'
function PerformanceDemo() {
const [input, setInput] = useState('')
const [isPending, startTransition] = useTransition()
// Version différée de l'input pour les calculs lourds
const deferredInput = useDeferredValue(input)
// Simulation d'une recherche très coûteuse
const searchResults = useMemo(() => {
if (!deferredInput) return []
console.log('🔍 Recherche coûteuse pour:', deferredInput)
// Simulation de 10000 résultats avec calculs
const results = []
for (let i = 0; i < 10000; i++) {
if (i.toString().includes(deferredInput)) {
results.push({
id: i,
title: `Résultat ${i} pour "${deferredInput}"`,
score: Math.random() * 100
})
}
}
return results.slice(0, 100) // Limiter à 100 résultats
}, [deferredInput])
const handleInputChange = (value) => {
// Mise à jour urgente de l'input (responsive)
setInput(value)
// Démarrer une transition pour les mises à jour moins prioritaires
startTransition(() => {
// Cette partie sera différée si l'app est occupée
console.log('🔄 Transition démarrée pour la recherche')
})
}
return (
<div>
<h3>Performance avec Transitions</h3>
<div>
<input
value={input}
onChange={e => handleInputChange(e.target.value)}
placeholder="Rechercher (taper des nombres)..."
style={{
padding: '10px',
width: '300px',
fontSize: '16px'
}}
/>
{isPending && <span style={{ marginLeft: '10px' }}>⏳ Recherche...</span>}
</div>
<div style={{ marginTop: '20px' }}>
<p>
Input actuel: "{input}" |
Recherche pour: "{deferredInput}" |
Résultats: {searchResults.length}
</p>
{deferredInput !== input && (
<p style={{ color: '#007bff' }}>
🔄 Recherche en cours pour "{input}"...
</p>
)}
</div>
<div style={{
maxHeight: '300px',
overflow: 'auto',
border: '1px solid #ddd',
marginTop: '10px'
}}>
{searchResults.map(result => (
<div key={result.id} style={{
padding: '8px',
borderBottom: '1px solid #eee'
}}>
<strong>{result.title}</strong>
<span style={{
float: 'right',
color: '#666',
fontSize: '14px'
}}>
Score: {result.score.toFixed(2)}
</span>
</div>
))}
</div>
</div>
)
}
Cas Pratique : Liste Filtrée
function LargeFilterableList() {
const [filter, setFilter] = useState('')
const [sortBy, setSortBy] = useState('name')
const [isPending, startTransition] = useTransition()
const deferredFilter = useDeferredValue(filter)
const deferredSortBy = useDeferredValue(sortBy)
// Dataset large simulé
const allItems = useMemo(() => {
return Array.from({ length: 50000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
category: ['A', 'B', 'C'][i % 3],
price: Math.random() * 100,
rating: Math.random() * 5
}))
}, [])
// Filtrage et tri différés (non bloquants)
const processedItems = useMemo(() => {
console.log('🔍 Processing items with filter:', deferredFilter)
let filtered = allItems
if (deferredFilter) {
filtered = allItems.filter(item =>
item.name.toLowerCase().includes(deferredFilter.toLowerCase()) ||
item.category.toLowerCase().includes(deferredFilter.toLowerCase())
)
}
// Tri
filtered.sort((a, b) => {
switch (deferredSortBy) {
case 'name': return a.name.localeCompare(b.name)
case 'price': return b.price - a.price // Décroissant
case 'rating': return b.rating - a.rating // Décroissant
default: return 0
}
})
return filtered.slice(0, 1000) // Limiter l'affichage
}, [allItems, deferredFilter, deferredSortBy])
const handleFilterChange = (value) => {
setFilter(value)
startTransition(() => {
// Cette mise à jour est différée
console.log('🔄 Filter transition started')
})
}
const handleSortChange = (value) => {
startTransition(() => {
setSortBy(value)
})
}
return (
<div>
<h3>Liste Filtrable Performance ({allItems.length} items)</h3>
<div style={{ marginBottom: '20px' }}>
<input
value={filter}
onChange={e => handleFilterChange(e.target.value)}
placeholder="Filtrer par nom ou catégorie..."
style={{
padding: '8px',
marginRight: '10px',
width: '250px'
}}
/>
<select
value={sortBy}
onChange={e => handleSortChange(e.target.value)}
style={{ padding: '8px' }}
>
<option value="name">Trier par nom</option>
<option value="price">Trier par prix</option>
<option value="rating">Trier par note</option>
</select>
{isPending && <span style={{ marginLeft: '10px' }}>⏳ Traitement...</span>}
</div>
<div style={{ marginBottom: '10px' }}>
<p>
Filtre: "{filter}" |
Traitement: "{deferredFilter}" |
Résultats: {processedItems.length}
</p>
</div>
<div style={{
height: '400px',
overflow: 'auto',
border: '1px solid #ddd'
}}>
{processedItems.map(item => (
<div key={item.id} style={{
padding: '12px',
borderBottom: '1px solid #eee',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<strong>{item.name}</strong>
<span style={{
marginLeft: '10px',
color: '#666',
fontSize: '14px'
}}>
Catégorie: {item.category}
</span>
</div>
<div style={{ textAlign: 'right' }}>
<div>{item.price.toFixed(2)}€</div>
<div>⭐ {item.rating.toFixed(1)}</div>
</div>
</div>
))}
</div>
</div>
)
}
useImperativeHandle - API Personnalisées
Forward Ref avec API Custom
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
// Composant avec API personnalisée exposée
const AdvancedInput = forwardRef((props, ref) => {
const inputRef = useRef()
const [history, setHistory] = useState([])
const [currentValue, setCurrentValue] = useState('')
// Exposer une API personnalisée au parent
useImperativeHandle(ref, () => ({
// Méthodes de focus
focus: () => inputRef.current?.focus(),
blur: () => inputRef.current?.blur(),
// Méthodes de valeur
getValue: () => currentValue,
setValue: (value) => {
setCurrentValue(value)
addToHistory(value)
},
clear: () => {
setCurrentValue('')
inputRef.current?.focus()
},
// Méthodes de sélection
selectAll: () => inputRef.current?.select(),
getSelection: () => ({
start: inputRef.current?.selectionStart || 0,
end: inputRef.current?.selectionEnd || 0
}),
setSelection: (start, end) => {
inputRef.current?.setSelectionRange(start, end)
},
// Historique
getHistory: () => history,
clearHistory: () => setHistory([]),
undo: () => {
if (history.length > 1) {
const newHistory = history.slice(0, -1)
const previousValue = newHistory[newHistory.length - 1] || ''
setHistory(newHistory)
setCurrentValue(previousValue)
return previousValue
}
return currentValue
},
// Validation
isValid: () => {
if (props.required && !currentValue.trim()) return false
if (props.minLength && currentValue.length < props.minLength) return false
if (props.pattern && !new RegExp(props.pattern).test(currentValue)) return false
return true
},
// Animation
shake: () => {
if (inputRef.current) {
inputRef.current.style.animation = 'shake 0.5s'
setTimeout(() => {
if (inputRef.current) {
inputRef.current.style.animation = ''
}
}, 500)
}
}
}), [currentValue, history, props.required, props.minLength, props.pattern])
const addToHistory = (value) => {
setHistory(prev => {
const newHistory = [...prev, value]
return newHistory.slice(-10) // Garder seulement les 10 derniers
})
}
const handleChange = (e) => {
const newValue = e.target.value
setCurrentValue(newValue)
addToHistory(newValue)
props.onChange?.(e)
}
return (
<div>
<input
ref={inputRef}
{...props}
value={currentValue}
onChange={handleChange}
style={{
padding: '10px',
border: '2px solid #ddd',
borderRadius: '4px',
fontSize: '16px',
...props.style
}}
/>
<style jsx>{`
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
`}</style>
</div>
)
})
AdvancedInput.displayName = 'AdvancedInput'
// Utilisation du composant avec API personnalisée
function ImperativeHandleDemo() {
const inputRef1 = useRef()
const inputRef2 = useRef()
const inputRef3 = useRef()
const handleValidate = () => {
const inputs = [inputRef1, inputRef2, inputRef3]
let allValid = true
inputs.forEach(inputRef => {
if (inputRef.current && !inputRef.current.isValid()) {
inputRef.current.shake()
allValid = false
}
})
if (allValid) {
alert('Tous les champs sont valides !')
} else {
alert('Certains champs ne sont pas valides')
}
}
const handleGetValues = () => {
const values = {
name: inputRef1.current?.getValue() || '',
email: inputRef2.current?.getValue() || '',
phone: inputRef3.current?.getValue() || ''
}
console.log('Valeurs actuelles:', values)
alert(JSON.stringify(values, null, 2))
}
const handleClearAll = () => {
;[inputRef1, inputRef2, inputRef3].forEach(ref => {
ref.current?.clear()
})
}
const handleFillExample = () => {
inputRef1.current?.setValue('Andy Cinquin')
inputRef2.current?.setValue('andy@example.com')
inputRef3.current?.setValue('0123456789')
}
return (
<div>
<h3>useImperativeHandle Demo</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', maxWidth: '400px' }}>
<div>
<label style={{ display: 'block', marginBottom: '5px' }}>
Nom (requis, min 3 caractères)
</label>
<AdvancedInput
ref={inputRef1}
placeholder="Votre nom..."
required
minLength={3}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '5px' }}>
Email (requis, format email)
</label>
<AdvancedInput
ref={inputRef2}
placeholder="votre@email.com"
required
pattern="[^@]+@[^@]+\.[^@]+"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '5px' }}>
Téléphone (10 chiffres)
</label>
<AdvancedInput
ref={inputRef3}
placeholder="0123456789"
pattern="[0-9]{10}"
/>
</div>
</div>
<div style={{ marginTop: '20px', display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
<button onClick={() => inputRef1.current?.focus()}>
Focus Nom
</button>
<button onClick={() => inputRef2.current?.selectAll()}>
Sélectionner Email
</button>
<button onClick={() => inputRef3.current?.undo()}>
Undo Téléphone
</button>
<button onClick={handleValidate}>
Valider Formulaire
</button>
<button onClick={handleGetValues}>
Voir Valeurs
</button>
<button onClick={handleClearAll}>
Tout Effacer
</button>
<button onClick={handleFillExample}>
Remplir Exemple
</button>
</div>
<div style={{ marginTop: '20px' }}>
<button onClick={() => {
const history = inputRef1.current?.getHistory() || []
console.log('Historique nom:', history)
alert('Historique: ' + history.join(' → '))
}}>
Voir Historique Nom
</button>
</div>
</div>
)
}
useDebugValue - Debug Custom Hooks
Hook de Debug
import { useDebugValue, useState, useEffect } from 'react'
// Hook custom avec debug
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine)
// Afficher des infos de debug dans React DevTools
useDebugValue(isOnline ? 'En ligne' : 'Hors ligne')
useEffect(() => {
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
return isOnline
}
// Hook custom avec debug complexe
function useApi(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// Debug value avec objet et formatage conditionnel
useDebugValue(
{ url, loading, hasData: !!data, error: !!error },
({ url, loading, hasData, error }) => {
if (loading) return `🔄 Loading: ${url}`
if (error) return `❌ Error: ${url}`
if (hasData) return `✅ Success: ${url}`
return `⏳ Idle: ${url}`
}
)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
setError(null)
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const result = await response.json()
setData(result)
} catch (err) {
setError(err)
} finally {
setLoading(false)
}
}
fetchData()
}, [url])
return { data, loading, error }
}
// Hook custom pour formulaire avec debug détaillé
function useForm(initialValues, validation) {
const [values, setValues] = useState(initialValues)
const [errors, setErrors] = useState({})
const [touched, setTouched] = useState({})
// Debug info complète
useDebugValue(
{
fieldCount: Object.keys(values).length,
errorCount: Object.keys(errors).length,
touchedCount: Object.keys(touched).length,
isValid: Object.keys(errors).length === 0,
values,
errors
},
(info) => {
const status = info.isValid ? '✅' : '❌'
return `${status} Form: ${info.fieldCount} fields, ${info.errorCount} errors, ${info.touchedCount} touched`
}
)
const setValue = (field, value) => {
setValues(prev => ({ ...prev, [field]: value }))
// Validation en temps réel
if (validation && validation[field]) {
const error = validation[field](value)
setErrors(prev => ({
...prev,
[field]: error
}))
}
}
const setTouched = (field) => {
setTouched(prev => ({ ...prev, [field]: true }))
}
const reset = () => {
setValues(initialValues)
setErrors({})
setTouched({})
}
return {
values,
errors,
touched,
setValue,
setTouched,
reset,
isValid: Object.keys(errors).length === 0
}
}
// Utilisation avec debug
function DebugDemo() {
const isOnline = useOnlineStatus()
const { data: users, loading, error } = useApi('https://jsonplaceholder.typicode.com/users?_limit=3')
const form = useForm(
{ name: '', email: '' },
{
name: (value) => value.length < 3 ? 'Nom trop court' : null,
email: (value) => !/\S+@\S+\.\S+/.test(value) ? 'Email invalide' : null
}
)
return (
<div>
<h3>Debug Demo - Ouvre React DevTools ! 🛠️</h3>
<div style={{ marginBottom: '20px' }}>
<p>Statut connexion: {isOnline ? '🟢 En ligne' : '🔴 Hors ligne'}</p>
<p>API: {loading ? 'Chargement...' : error ? 'Erreur' : `${users?.length || 0} users`}</p>
</div>
<form>
<div>
<label>
Nom:
<input
value={form.values.name}
onChange={e => form.setValue('name', e.target.value)}
onBlur={() => form.setTouched('name')}
/>
</label>
{form.errors.name && form.touched.name && (
<span style={{ color: 'red' }}>{form.errors.name}</span>
)}
</div>
<div>
<label>
Email:
<input
value={form.values.email}
onChange={e => form.setValue('email', e.target.value)}
onBlur={() => form.setTouched('email')}
/>
</label>
{form.errors.email && form.touched.email && (
<span style={{ color: 'red' }}>{form.errors.email}</span>
)}
</div>
<button type="button" onClick={form.reset}>
Reset
</button>
</form>
<div style={{ marginTop: '20px', background: '#f8f9fa', padding: '10px' }}>
<h4>Ouvre React DevTools pour voir les infos de debug !</h4>
<p>Dans l'onglet Components, cherche les hooks personnalisés</p>
<p>Form valide: {form.isValid ? '✅' : '❌'}</p>
</div>
</div>
)
}