Custom Hook β Encapsulating Reusable Logic
\\n\\nIn this chapter, you will learn React best practices: using custom Hooks to encapsulate reusable logic, making components cleaner and easier to maintain.
\\n\\n\\n\\n
What Is a Custom Hook?
\\n\\nAs functionality grows, useState and useEffect become scattered across components, leading to duplicated logic between components.
A custom Hook extracts the combined logic of multiple Hooks into a function named with use at the beginning.
Its essence: a plain JavaScript function that internally uses React Hooks.
\\n\\n\\n\\n
Why Do We Need Custom Hooks?
\\n\\n| Without Custom Hooks | \\nWith Custom Hooks | \\n
|---|---|
| Each component writes its own fetch + filtering logic | \\nOne usePosts() reused everywhere | \\n
| Data logic mixed with UI logic | \\nData logic is decoupled; components only handle UI | \\n
| Changing one logic requires modifying multiple files | \\nChange once, all components update automatically | \\n
| Components often exceed 200+ lines | \\nComponents usually under 80 lines | \\n
\\n\\n
Naming Conventions & Hook Rules
\\n\\n| Convention | \\nDescription | \\nExample | \\n
|---|---|---|
Start with use | \\n React uses this to identify Hooks and apply linting rules | \\nusePosts, useDarkMode | \\n
Place in hooks/ directory | \\n Convenient for searching and maintenance | \\nsrc/hooks/usePosts.js | \\n
| Can only be called at the top level of a function component | \\nCannot be called inside conditions, loops, or nested functions | \\nβ | \\n
| Return data or methods | \\nAllow callers to access internal state and logic of the Hook | \\nreturn { data, isLoading } | \\n
\\n\\n\\nHook usage rules: 1. Only call Hooks at the top level of function components; 2. Only call Hooks inside React functions (function components or custom Hooks). This ensures consistent Hook call order across renders.
\\n
\\n\\n
usePosts β Encapsulating Article Data Logic
\\n\\nExample
\\n\\n// File path: src/hooks/usePosts.js\\n\\nimport { useState, useEffect, useMemo } from 'react'\\n\\nexport function usePosts() {\\n\\n const [articles, setArticles] = useState([])\\n const [isLoading, setIsLoading] = useState(true)\\n const [error, setError] = useState(null)\\n const [activeCategory, setActiveCategory] = useState('All')\\n const [keyword, setKeyword] = useState('')\\n\\n // Load data\\n useEffect(() => {\\n let cancelled = false\\n\\n async function fetchPosts() {\\n setIsLoading(true)\\n setError(null)\\n try {\\n const res = await fetch('/posts.json')\\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\\n const data = await res.json()\\n if (!cancelled) setArticles(data)\\n } catch (err) {\\n if (!cancelled) setError(err.message)\\n } finally {\\n if (!cancelled) setIsLoading(false)\\n }\\n }\\n\\n fetchPosts()\\n\\n return () => { cancelled = true }\\n }, [])\\n\\n // Extract all categories\\n const categories = useMemo(() => {\\n return ['All', ...new Set(articles.map(a => a.category))]\\n }, )\\n\\n // Filter by category + keyword\\n const filteredArticles = useMemo(() => {\\n let result = articles\\n if (activeCategory !== 'All') {\\n result = result.filter(a => a.category === activeCategory)\\n }\\n if (keyword.trim()) {\\n const kw = keyword.trim().toLowerCase()\\n result = result.filter(a =>\\n a.title.toLowerCase().includes(kw) ||\\n a.summary.toLowerCase().includes(kw)\\n )\\n }\\n return result\\n }, [articles, activeCategory, keyword])\\n\\n // Find article by ID\\n function getArticleById(id) {\\n return articles.find(a => a.id === Number(id))\\n }\\n\\n // Refetch data\\n function refetch() {\\n // Alternative way to trigger useEffect: using key or enhanced state\\n window.location.reload()\\n }\\n\\n return {\\n articles, isLoading, error,\\n activeCategory, setActiveCategory,\\n keyword, setKeyword,\\n categories,\\n filteredArticles,\\n getArticleById,\\n refetch\\n }\\n}\\n\\n\\nUsing usePosts in the Homepage
\\n\\nExample
\\n\\n// File path: src/pages/HomePage.jsx\\n\\nimport { usePosts } from '../hooks/usePosts'\\nimport BlogCard from '../components/BlogCard'\\nimport CategoryFilter from '../components/CategoryFilter'\\n\\nfunction HomePage() {\\n // One-line code to get all article-related data and operations\\n const {\\n isLoading, error,\\n activeCategory, setActiveCategory,\\n keyword, setKeyword,\\n categories,\\n filteredArticles,\\n refetch\\n } = usePosts()\\n\\n if (isLoading) return <p className="status-msg">Loading...</p>\\n if (error) return (\\n <div className="status-msg error">\\n <p>Load failed:{error}</p>\\n <button onClick={refetch}>Retry</button>\\n </div>\\n )\\n\\n return (\\n <div>\\n <h2 className="section-title">Latest Posts</h2>\\n <div className="search-bar">\\n <input\\n type="text"\\n value={keyword}\\n onChange={e => setKeyword(e.target.value)}\\n placeholder="Search article titles or summaries..."\\n className="search-input"\\n />\\n {keyword && <span className="clear-btn" onClick={() => setKeyword('')}>β</span>}\\n </div>\\n <CategoryFilter\\n categories={categories}\\n activeCategory={activeCategory}\\n onCategoryChange={setActiveCategory}\\n />\\n <p className="result-info">Total {filteredArticles.length} posts</p>\\n {filteredArticles.length === 0 ? (\\n <p className="empty-tip">No matching articles</p>\\n ) : (\\n <div className="article-grid">\\n {filteredArticles.map(article => (\\n <BlogCard key={article.id} {...article} />\\n ))}\\n </div>\\n )}\\n </div>\\n )\\n}\\n\\nexport default HomePage\\n\\n\\nUsing usePosts in the Detail Page
\\n\\nExample
\\n\\n// File path: src/pages/PostPage.jsx\\n\\nimport { useParams, Link } from 'react-router-dom'\\nimport { usePosts } from '../hooks/usePosts'\\n\\nfunction PostPage() {\\n const { id } = useParams()\\n const { isLoading, error, getArticleById } = usePosts()\\n const article = getArticleById(id)\\n\\n if (isLoading) return <p className="status-msg">Loading...</p>\\n if (error) return <p className="status-msg error">Load failed:{error}</p>\\n if (!article) {\\n return (\\n <div className="not-found">\\n <h2>Post not found</h2>\\n <Link to="/"></Link>\\n </div>\\n )\\n }\\n\\n return (\\n <article className="post-view">\\n <span className="category-tag">{article.category}</span>\\n <h1>{article.title}</h1>\\n <time>{article.date}</time>\\n <div className="content" dangerouslySetInnerHTML={{ __html: article.content }} />\\n <Link to="/" className="back-link">β </Link>\\n </article>\\n )\\n}\\n\\nexport default PostPage\\n\\n\\n\\n\\n\\nNote: If the user directly accesses the detail page URL (e.g., by refreshing),
\\nusePostswill re-fetch data. This is correct behaviorβbecause theusePostsinstance on the homepage and the one on the detail page are independent, each calling its ownuseEffect.
\\n\\n
useDarkMode β One-Line Dark Mode Toggle
\\n\\nExample
\\n\\n// File path: src/hooks/useDarkMode.js\\n\\nimport { useState, useEffect } from 'react'\\n\\nexport function useDarkMode() {\\n // Read previous setting from localStorage\\n const [isDark, setIsDark] = useState(() => {\\n return localStorage.getItem('blog-theme') === 'dark'\\n })\\n\\n // Sync to DOM and localStorage when isDark changes\\n useEffect(() => {\\n if (isDark) {\\n document.documentElement.classList.add('dark')\\n localStorage.setItem('blog-theme', 'dark')\\n } else {\\n document.documentElement.classList.remove('dark')\\n localStorage.setItem('blog-theme', 'light')\\n }\\n }, )\\n\\n function toggleDark() {\\n setIsDark(!isDark)\\n }\\n\\n return { isDark, toggleDark }\\n}\\n\\n\\nUsage in NavBar:
\\n\\nExample
\\n\\n// File path: src/components/NavBar.jsx\\n\\nimport { useDarkMode } from '../hooks/useDarkMode'\\n\\nfunction NavBar() {\\n const { isDark, toggleDark } = useDarkMode()\\n\\n return (\\n <header className="navbar">\\n <a href="/" className="logo">TUTORIAL Blog</a>\\n <nav>\\n <a href="/">Home</a>\\n <button className="theme-btn" onClick={toggleDark}>\\n {isDark ? '☀ Light' : 'βΎ Dark'}\\n </button>\\n </nav>\\n </header>\\n )\\n}\\n\\nexport default NavBar\\n\\n\\nTheme Switching via CSS Variables
\\n\\nExample
\\n\\n/* File path: src/index.css (global styles) */\\n\\n/* Light mode (default) */\\n:root {\\n --bg-primary: #f5f5f5;\\n --bg-card: #ffffff;\\n --text-primary: #333333;\\n --text-secondary: #666666;\\n --border-color: #eeeeee;\\n}\\n\\n/* Dark mode */\\nhtml.dark {\\n --bg-primary: #1a1a2e;\\n --bg-card: #16213e;\\n --text-primary: #e0e0e0;\\n --text-secondary: #a0a0a0;\\n --border-color: #2a2a4a;\\n}\\n\\nbody {\\n background: var(--bg-primary);\\n color: var(--text-primary);\\n}\\n\\n.card, .article-card {\\n background: var(--bg-card);\\n border: 1px solid var(--border-color);\\n}\\n\\n\\n\\n\\n
React Custom Hooks vs Vue3 Composables
\\n\\n| Feature | \\nReact Custom Hook | \\nVue3 Composable | \\n
|---|---|---|
| Naming convention | \\nStarts with use | \\n Starts with use | \\n
| Essence | \\nA function using React Hooks | \\nA function using Vue3 APIs | \\n
| Reactivity | \\nTriggered via setter returned by useState | \\n Automatically tracked via ref/reactive | \\n
| Calling restrictions | \\nOnly at top level of function component | \\nOnly at top level of <script setup> | \\n
| Side effects | \\nuseEffect | \\n watch / watchEffect / onMounted | \\n
\\n\\n
Chapter Summary
\\n\\nIn this chapter, you mastered Reactβs most important design patternβcustom Hooks: named with use, internally composed of multiple React Hooks, and returning state and operations.
usePosts unifies data fetching, filtering, and searching; useDarkMode enables dark mode with one line of code. Components now only focus on UI rendering.
YouTip