Hooks personnalisés — Encapsulation de la logique réutilisable
Dans ce chapitre, vous apprendrez les bonnes pratiques React — utiliser des Hooks personnalisés pour encapsuler une logique réutilisable, ce qui rend les composants plus propres et plus faciles à maintenir.
Qu'est-ce qu'un Hook personnalisé ?
À mesure que les fonctionnalités se développent, useState et useEffect sont dispersés dans les composants, et de la logique dupliquée existe entre différents composants.
Un Hook personnaliséextrait la logique combinée de plusieurs Hooks dans une fonction qui commence par « use ».
Son essence :Une fonction JS ordinaire qui utilise en interne les Hooks React。
Pourquoi avez-vous besoin de Hooks personnalisés ?
| Sans Hooks personnalisés | Avec des Hooks personnalisés |
|---|---|
| Chaque composant écrit sa propre logique de fetch + filter | Un seul usePosts() est réutilisé partout |
| La logique de données et la logique UI sont mélangées | La logique de données est indépendante, les composants ne s'occupent que de l'UI |
| Modifier une partie de la logique nécessite de modifier plusieurs fichiers | Modifiez un seul endroit, tous les composants se mettent à jour de manière synchrone |
| Les composants dépassent souvent 200+ lignes | Les composants font généralement < 80 lignes |
Conventions de nommage et règles des Hooks
| Règle | Description | Exemple |
|---|---|---|
withusePréfixe | React utilise cela pour reconnaître les Hooks et appliquer les règles de lint | usePosts, useDarkMode |
| À placer dans le répertoire hooks/ | Pour faciliter la recherche et la maintenance | src/hooks/usePosts.js |
| Ne peut être appelé qu'au niveau supérieur des composants fonction | Ne peut pas être appelé à l'intérieur de conditions, de boucles ou de fonctions imbriquées | — |
| Retourne des données ou des méthodes | Permet à l'appelant d'accéder à l'état et à la logique à l'intérieur du Hook | return { data, isLoading } |
Règles d'utilisation des Hooks : 1. Appelez uniquement les Hooks au niveau supérieur des composants fonction ; 2. Appelez uniquement les Hooks dans les fonctions React (composants fonction ou Hooks personnalisés). Cela garantit que l'ordre des appels de Hook est cohérent à chaque rendu.
usePosts — Encapsulation de la logique des données de publication
Exemple
import { useState, useEffect, useMemo } from 'react'
export function usePosts() {
const [articles, setArticles] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
const [activeCategory, setActiveCategory] = useState('Toutes')
const [keyword, setKeyword] = useState('')
// Charger les données
useEffect(() => {
let cancelled = false
async function fetchPosts() {
setIsLoading(true)
setError(null)
try {
const res = await fetch('/posts.json')
if (!res.ok) throw new Erreur(`HTTP ${res.status}`)
const data = await res.json()
if (!cancelled) setArticles(data)
} catch (err) {
if (!cancelled) setError(err.message)
} finally {
if (!cancelled) setIsLoading(false)
}
}
fetchPosts()
return () => { cancelled = true }
}, [])
// Extraire toutes les catégories
const categories = useMemo(() => {
return ['Toutes', ...new Set(articles.map(a => a.category))]
}, [articles])
// Filtrer par catégorie + mot-clé
const filteredArticles = useMemo(() => {
let result = articles
if (activeCategory !== 'All') {
result = result.filter(a => a.category === activeCategory)
}
if (keyword.trim()) {
const kw = keyword.trim().toLowerCase()
result = result.filter(a =>
a.title.toLowerCase().includes(kw) ||
a.summary.toLowerCase().includes(kw)
)
}
return result
}, [articles, activeCategory, keyword])
// Trouver l'article par ID
function getArticleById(id) {
return articles.find(a => a.id === Number(id))
}
// Recharger
function refetch() {
// Une autre façon de déclencher useEffect : utiliser une clé ou améliorer l'état
window.location.reload()
}
return {
articles, isLoading, error,
activeCategory, setActiveCategory,
keyword, setKeyword,
categories,
filteredArticles,
getArticleById,
refetch
}
}
Page d'accueil utilisant usePosts
Exemple
import { usePosts } from '../hooks/usePosts'
import BlogCard from '../components/BlogCard'
import CategoryFilter from '../components/CategoryFilter'
function HomePage() {
// Récupérez toutes les données et opérations liées aux articles en une seule ligne de code
const {
isLoading, error,
activeCategory, setActiveCategory,
keyword, setKeyword,
categories,
filteredArticles,
refetch
} = usePosts()
if (isLoading) return <p className="status-msg">Chargement...</p>
if (error) return (
<div className="status-msg error">
<p>Échec du chargement :{error}</p>
<button onClick={refetch}>Réessayer</button>
</div>
)
return (
<div>
<h2 className="section-title">Derniers articles</h2>
<div className="search-bar">
<input
type="text"
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="Rechercher dans les titres ou résumés des articles..."
className="search-input"
/>
{keyword && <span className="clear-btn" onClick={() => setKeyword('')}>✕</span>}
</div>
<CategoryFilter
categories={categories}
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<p className="result-info">common{filteredArticles.length}article</p>
{filteredArticles.length === 0 ? (
<p className="empty-tip">Aucun article correspondant</p>
) : (
<div className="article-grid">
{filteredArticles.map(article => (
<BlogCard key={article.id} {...article} />
))}
</div>
)}
</div>
)
}
export default HomePage
Page de détail utilisant usePosts
Exemple
import { useParams, Link } from 'react-router-dom'
import { usePosts } from '../hooks/usePosts'
function PostPage() {
const { id } = useParams()
const { isLoading, error, getArticleById } = usePosts()
const article = getArticleById(id)
if (isLoading) return <p className="status-msg">Chargement...</p>
if (error) return <p className="status-msg error">Échec du chargement :{error}</p>
if (!article) {
return (
<div className="not-found">
<h2>Article introuvable</h2>
<Link to="/">Retour à l'accueil</Link>
</div>
)
}
return (
<article className="post-view">
<span className="category-tag">{article.category}</span>
<h1>{article.title}</h1>
<time>{article.date}</time>
<div className="content" dangerouslySetInnerHTML={{ __html: article.content }} />
<Link to="/" className="back-link">← Retour à l'accueil</Link>
</article>
)
}
export default PostPage
Note : Si l'utilisateur visite directement l'URL de la page de détail (par exemple, en actualisant la page), usePosts va re-récupérer les données. C'est le comportement correct — car l'instance usePosts sur la page d'accueil et l'instance usePosts sur la page de détail sont indépendantes, chacune appelant son propre useEffect.
useDarkMode — Passer en mode sombre avec une seule ligne de code
Exemple
import { useState, useEffect } from 'react'
export function useDarkMode() {
// Lire le paramètre précédent depuis localStorage
const [isDark, setIsDark] = useState(() => {
return localStorage.getItem('blog-theme') === 'dark'
})
// Lorsque isDark change, synchroniser avec le DOM et localStorage
useEffect(() => {
if (isDark) {
document.documentElement.classList.add('dark')
localStorage.setItem('blog-theme', 'dark')
} else {
document.documentElement.classList.remove('dark')
localStorage.setItem('blog-theme', 'light')
}
}, [isDark])
function toggleDark() {
setIsDark(!isDark)
}
return { isDark, toggleDark }
}
Utilisation dans NavBar :
Exemple
import { useDarkMode } from '../hooks/useDarkMode'
function NavBar() {
const { isDark, toggleDark } = useDarkMode()
return (
<header className="navbar">
<a href="/" className="logo">EXEMPLE Blog</a>
<nav>
<a href="/">Accueil</a>
<button className="theme-btn" onClick={toggleDark}>
{isDark ? '☀ Clair' : '☾ Sombre'}
</button>
</nav>
</header>
)
}
export default NavBar
Implémentation du changement de thème avec des variables CSS
Exemple
/* Mode clair (par défaut) */
:root {
--bg-primary: #f5f5f5;
--bg-card: #ffffff;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #eeeeee;
}
/* Mode sombre */
html.dark {
--bg-primary: #1a1a2e;
--bg-card: #16213e;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--border-color: #2a2a4a;
}
body {
background: var(--bg-primary);
color: var(--text-primary);
}
.card, .article-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
}
Hooks personnalisés React vs Composables Vue3
| Fonctionnalité | Hook personnalisé React | Composable Vue3 |
|---|---|---|
| Convention de nommage | Commence par "use" | Commence par "use" |
| Essence | Une fonction qui utilise des Hooks React | Une fonction qui utilise des API Vue3 |
| Réactivité | Déclenché via le setter retourné par useState | Suivi automatiquement via ref/reactive |
| Restriction d'appel | Uniquement au niveau supérieur des composants fonction | Uniquement au niveau supérieur de <script setup> |
| Effets de bord | useEffect | watch / watchEffect / onMounted |
Résumé du chapitre
Dans ce chapitre, vous avez maîtrisé le modèle de conception le plus important de React — les Hooks personnalisés : commençant par « use », combinant en interne plusieurs Hooks React, et renvoyant l'état et les opérations.
usePosts gère de manière centralisée le chargement des données + le filtrage + la recherche, et useDarkMode permet l'intégration du mode sombre avec une seule ligne de code. Les composants ne se soucient désormais que du rendu de l'interface utilisateur.
Autres extensions