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ésAvec des Hooks personnalisés
Chaque composant écrit sa propre logique de fetch + filterUn seul usePosts() est réutilisé partout
La logique de données et la logique UI sont mélangéesLa 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 fichiersModifiez un seul endroit, tous les composants se mettent à jour de manière synchrone
Les composants dépassent souvent 200+ lignesLes composants font généralement < 80 lignes

Conventions de nommage et règles des Hooks

RègleDescriptionExemple
withusePréfixeReact utilise cela pour reconnaître les Hooks et appliquer les règles de lintusePosts, useDarkMode
À placer dans le répertoire hooks/Pour faciliter la recherche et la maintenancesrc/hooks/usePosts.js
Ne peut être appelé qu'au niveau supérieur des composants fonctionNe peut pas être appelé à l'intérieur de conditions, de boucles ou de fonctions imbriquées—
Retourne des données ou des méthodesPermet à l'appelant d'accéder à l'état et à la logique à l'intérieur du Hookreturn { 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

// Chemin du fichier : src/hooks/usePosts.js
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

// Chemin du fichier : src/pages/HomePage.jsx
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

// Chemin du fichier : src/pages/PostPage.jsx
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

// Chemin du fichier : src/hooks/useDarkMode.js
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

// Chemin du fichier : src/components/NavBar.jsx
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 ? '&#x2600; Clair' : '☾ Sombre'}
        </button>
      </nav>
    </header>
  )
}

export default NavBar

Implémentation du changement de thème avec des variables CSS

Exemple

/* Chemin du fichier : src/index.css (styles globaux) */
/* 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é ReactComposable Vue3
Convention de nommageCommence par "use"Commence par "use"
EssenceUne fonction qui utilise des Hooks ReactUne fonction qui utilise des API Vue3
RéactivitéDéclenché via le setter retourné par useStateSuivi automatiquement via ref/reactive
Restriction d'appelUniquement au niveau supérieur des composants fonctionUniquement au niveau supérieur de <script setup>
Effets de borduseEffectwatch / 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