YouTip LogoYouTip

React Blog Custom Hooks

Custom Hook β€” Encapsulating Reusable Logic

\\n\\n

In 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\\n

As functionality grows, useState and useEffect become scattered across components, leading to duplicated logic between components.

\\n\\n

A custom Hook extracts the combined logic of multiple Hooks into a function named with use at the beginning.

\\n\\n

Its essence: a plain JavaScript function that internally uses React Hooks.

\\n\\n
\\n\\n

Why Do We Need Custom Hooks?

\\n\\n\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n
Without Custom HooksWith Custom Hooks
Each component writes its own fetch + filtering logicOne usePosts() reused everywhere
Data logic mixed with UI logicData logic is decoupled; components only handle UI
Changing one logic requires modifying multiple filesChange once, all components update automatically
Components often exceed 200+ linesComponents usually under 80 lines
\\n\\n
\\n\\n

Naming Conventions & Hook Rules

\\n\\n\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n
ConventionDescriptionExample
Start with useReact uses this to identify Hooks and apply linting rulesusePosts, useDarkMode
Place in hooks/ directoryConvenient for searching and maintenancesrc/hooks/usePosts.js
Can only be called at the top level of a function componentCannot be called inside conditions, loops, or nested functionsβ€”
Return data or methodsAllow callers to access internal state and logic of the Hookreturn { data, isLoading }
\\n\\n
\\n

Hook 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
\\n\\n

usePosts β€” Encapsulating Article Data Logic

\\n\\n

Example

\\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\\n

Using usePosts in the Homepage

\\n\\n

Example

\\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\\n

Using usePosts in the Detail Page

\\n\\n

Example

\\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

Note: If the user directly accesses the detail page URL (e.g., by refreshing), usePosts will re-fetch data. This is correct behaviorβ€”because the usePosts instance on the homepage and the one on the detail page are independent, each calling its own useEffect.

\\n
\\n\\n
\\n\\n

useDarkMode β€” One-Line Dark Mode Toggle

\\n\\n

Example

\\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\\n

Usage in NavBar:

\\n\\n

Example

\\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\\n

Theme Switching via CSS Variables

\\n\\n

Example

\\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\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n \\n
FeatureReact Custom HookVue3 Composable
Naming conventionStarts with useStarts with use
EssenceA function using React HooksA function using Vue3 APIs
ReactivityTriggered via setter returned by useStateAutomatically tracked via ref/reactive
Calling restrictionsOnly at top level of function componentOnly at top level of <script setup>
Side effectsuseEffectwatch / watchEffect / onMounted
\\n\\n
\\n\\n

Chapter Summary

\\n\\n

In 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.

\\n\\n

usePosts unifies data fetching, filtering, and searching; useDarkMode enables dark mode with one line of code. Components now only focus on UI rendering.

← Vue3 Blog Components Props EmiReact Blog Useeffect β†’