Custom Hooks — Encapsulating Reusable Logic
In this chapter, you will learn React best practices — using custom Hooks to encapsulate reusable logic, making components cleaner and easier to maintain.
What is a Custom Hook?
As features grow, useState and useEffect are scattered across components, and duplicated logic exists between different components.
Custom Hookextracts the combined logic of multiple Hooks into a function that starts with "use".
Its essence:A regular JS function that internally uses React Hooks。
Why Do You Need Custom Hooks?
| Without Custom Hooks | With Custom Hooks |
|---|---|
| Each component writes its own fetch + filter logic | One usePosts() is reused everywhere |
| Data logic and UI logic are mixed together | Data logic is independent, components only care about UI |
| Changing one piece of logic requires modifying multiple files | Change one place, all components update synchronously |
| Components often exceed 200+ lines | Components are usually < 80 lines |
Naming Conventions and Hook Rules
| Rule | Description | Example |
|---|---|---|
withusePrefix | React uses this to recognize Hooks and apply lint rules | usePosts、useDarkMode |
| Place in the hooks/ directory | For easy lookup and maintenance | src/hooks/usePosts.js |
| Can only be called at the top level of function components | Cannot be called inside conditions, loops, or nested functions | — |
| Return data or methods | Allow the caller to access the state and logic inside the Hook | return { data, isLoading } |
Hook usage rules: 1. Only call Hooks at the top level of function components; 2. Only call Hooks in React functions (function components or custom Hooks). This ensures that the order of Hook calls is consistent on every render.
usePosts — Encapsulating Post Data Logic
Example
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('All')
const [keyword, setKeyword] = useState('')
// Load data
useEffect(() => {
let cancelled = false
async function fetchPosts() {
setIsLoading(true)
setError(null)
try {
const res = await fetch('/posts.json')
if (!res.ok) throw new Error(`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 }
}, [])
// Extract all categories
const categories = useMemo(() => {
return ['All', ...new Set(articles.map(a => a.category))]
}, [articles])
// Filter by category + keyword
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])
// Find post by ID
function getArticleById(id) {
return articles.find(a => a.id === Number(id))
}
// Reload
function refetch() {
// Another way to trigger useEffect: use a key or enhance state
window.location.reload()
}
return {
articles, isLoading, error,
activeCategory, setActiveCategory,
keyword, setKeyword,
categories,
filteredArticles,
getArticleById,
refetch
}
}
Home Page Using usePosts
Example
import { usePosts } from '../hooks/usePosts'
import BlogCard from '../components/BlogCard'
import CategoryFilter from '../components/CategoryFilter'
function HomePage() {
// Get all post-related data and operations with one line of code
const {
isLoading, error,
activeCategory, setActiveCategory,
keyword, setKeyword,
categories,
filteredArticles,
refetch
} = usePosts()
if (isLoading) return <p className="status-msg">Loading...</p>
if (error) return (
<div className="status-msg error">
<p>Load failed:{error}</p>
<button onClick={refetch}>Retry</button>
</div>
)
return (
<div>
<h2 className="section-title">Latest Posts</h2>
<div className="search-bar">
<input
type="text"
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="Search post titles or summaries..."
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">No matching posts</p>
) : (
<div className="article-grid">
{filteredArticles.map(article => (
<BlogCard key={article.id} {...article} />
))}
</div>
)}
</div>
)
}
export default HomePage
Detail Page Using usePosts
Example
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">Loading...</p>
if (error) return <p className="status-msg error">Load failed:{error}</p>
if (!article) {
return (
<div className="not-found">
<h2>Post not found</h2>
<Link to="/">Back to Home</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">← Back to Home</Link>
</article>
)
}
export default PostPage
Note: If the user directly visits the detail page URL (e.g., refreshing the page), usePosts will re-fetch the data. This is correct behavior — because the usePosts instance on the home page and the usePosts instance on the detail page are independent, each calling its own useEffect.
useDarkMode — Switch to Dark Mode with One Line of Code
Example
import { useState, useEffect } from 'react'
export function useDarkMode() {
// Read the previous setting from localStorage
const [isDark, setIsDark] = useState(() => {
return localStorage.getItem('blog-theme') === 'dark'
})
// When isDark changes, sync to DOM and 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 }
}
Using it in NavBar:
Example
import { useDarkMode } from '../hooks/useDarkMode'
function NavBar() {
const { isDark, toggleDark } = useDarkMode()
return (
<header className="navbar">
<a href="/" className="logo">EXAMPLE Blog</a>
<nav>
<a href="/">Home</a>
<button className="theme-btn" onClick={toggleDark}>
{isDark ? '☀ Light' : '☾ Dark'}
</button>
</nav>
</header>
)
}
export default NavBar
Implementing Theme Switching with CSS Variables
Example
/* Light mode (default) */
:root {
--bg-primary: #f5f5f5;
--bg-card: #ffffff;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #eeeeee;
}
/* Dark mode */
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);
}
React Custom Hooks vs Vue3 Composables
| Feature | React Custom Hook | Vue3 Composable |
|---|---|---|
| Naming convention | Starts with "use" | Starts with "use" |
| Essence | A function that uses React Hooks | A function that uses Vue3 APIs |
| Reactivity | Triggered via the setter returned by useState | Automatically tracked via ref/reactive |
| Call restriction | Only at the top level of function components | Only at the top level of <script setup> |
| Side effects | useEffect | watch / watchEffect / onMounted |
Chapter Summary
In this chapter, you have mastered React's most important design pattern — custom Hooks: starting with "use", internally combining multiple React Hooks, and returning state and operations.
usePosts centrally manages data loading + filtering + search, and useDarkMode enables dark mode integration with one line of code. Components now only care about UI rendering.
Other Extensions