React Hooks ขั้นสูง: รูปแบบและการเพิ่มประสิทธิภาพ
เชี่ยวชาญ React Hooks ขั้นสูงด้วยรูปแบบที่พิสูจน์แล้ว Custom hooks, useEffect ที่ปรับแต่ง, useMemo, useCallback และเทคนิคด้านประสิทธิภาพ

React Hooks เปลี่ยนแปลงวิธีจัดการ state และ side effect ในคอมโพเนนต์แบบฟังก์ชันอย่างสิ้นเชิง นอกเหนือจากการใช้งานพื้นฐานของ useState และ useEffect รูปแบบขั้นสูงช่วยให้สามารถสร้างโค้ดที่นำกลับมาใช้ใหม่ได้ มีประสิทธิภาพสูง และดูแลรักษาง่าย
คู่มือนี้สมมติว่าผู้อ่านคุ้นเคยกับ Hooks พื้นฐาน (useState, useEffect, useContext) ตัวอย่างใช้ React 18+ และ TypeScript เพื่อการระบุชนิดข้อมูลที่แข็งแกร่ง
เชี่ยวชาญ useEffect: หลีกเลี่ยงกับดักที่พบบ่อย
Hook useEffect มักถูกเข้าใจผิดและใช้งานไม่ถูกต้อง การเข้าใจการทำงานอย่างลึกซึ้งช่วยหลีกเลี่ยงบั๊กที่ตรวจจับได้ยากและปัญหาด้านประสิทธิภาพ
หลักการพื้นฐาน: useEffect ซิงโครไนซ์คอมโพเนนต์กับระบบภายนอก หาก effect ไม่สื่อสารกับโลกภายนอก (API, DOM, timer) ก็มีแนวโน้มว่าจะไม่จำเป็น
// Custom hook to synchronize document title
import { useEffect } from 'react'
export function useDocumentTitle(title: string) {
useEffect(() => {
// Save the previous title
const previousTitle = document.title
// Update the title
document.title = title
// Cleanup: restore previous title on unmount
return () => {
document.title = previousTitle
}
}, [title]) // Only trigger when title changes
}ข้อผิดพลาดที่พบบ่อยที่สุดคือการละเลย dependency array หรือใส่ค่าที่ไม่เสถียร ค่าทุกค่าที่ใช้ใน effect ต้องปรากฏใน dependencies
'use client'
import { useEffect, useState } from 'react'
interface User {
id: string
name: string
email: string
}
interface UserProfileProps {
userId: string
}
export function UserProfile({ userId }: UserProfileProps) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
// Flag to prevent state updates after unmount
let isMounted = true
// Controller to cancel pending requests
const controller = new AbortController()
async function fetchUser() {
setLoading(true)
setError(null)
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
})
if (!response.ok) {
throw new Error('User not found')
}
const data = await response.json()
// Check if component is still mounted
if (isMounted) {
setUser(data)
}
} catch (err) {
// Ignore abort errors
if (err instanceof Error && err.name !== 'AbortError') {
if (isMounted) {
setError(err.message)
}
}
} finally {
if (isMounted) {
setLoading(false)
}
}
}
fetchUser()
// Cleanup: abort request and mark as unmounted
return () => {
isMounted = false
controller.abort()
}
}, [userId]) // Re-run effect when userId changes
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!user) return null
return (
<div className="p-4 border rounded-lg">
<h2 className="text-xl font-bold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
</div>
)
}รูปแบบที่ใช้ AbortController และธง isMounted นี้ช่วยป้องกัน memory leak และการอัปเดต state บนคอมโพเนนต์ที่ถูก unmount แล้ว
สร้าง custom hooks ที่นำกลับมาใช้ใหม่ได้
Custom hooks ห่อหุ้มตรรกะที่นำกลับมาใช้ใหม่ได้ custom hook ที่ดียึดหลักความรับผิดชอบเดียวและให้ API ที่ชัดเจน
// Generic hook to persist state in localStorage
import { useState, useEffect, useCallback } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
// Initialize state with stored value or default
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.warn(`Error reading localStorage "${key}":`, error)
return initialValue
}
})
// Sync with localStorage on every change
useEffect(() => {
if (typeof window === 'undefined') return
try {
window.localStorage.setItem(key, JSON.stringify(storedValue))
} catch (error) {
console.warn(`Error writing localStorage "${key}":`, error)
}
}, [key, storedValue])
// Function to update value
const setValue = useCallback((value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const nextValue = value instanceof Function ? value(prev) : value
return nextValue
})
}, [])
// Function to remove value
const removeValue = useCallback(() => {
setStoredValue(initialValue)
if (typeof window !== 'undefined') {
window.localStorage.removeItem(key)
}
}, [key, initialValue])
return [storedValue, setValue, removeValue]
}สามารถใช้ hook นี้ในคอมโพเนนต์ใดก็ได้เพื่อจัดเก็บข้อมูลให้คงอยู่
'use client'
import { useLocalStorage } from '@/hooks/useLocalStorage'
type Theme = 'light' | 'dark' | 'system'
export function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<Theme>('theme', 'system')
return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value as Theme)}
className="px-3 py-2 border rounded-lg"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
)
}Custom hooks ขึ้นต้นด้วย "use" เสมอ (useLocalStorage, useFetch, useDebounce) ข้อตกลงนี้ช่วยให้ React ตรวจสอบกฎของ hook ได้และเครื่องมือ linting ทำงานได้อย่างถูกต้อง
รูปแบบการประกอบกันด้วย useReducer
สำหรับ state ที่ซับซ้อนซึ่งมีหลายแอ็กชัน useReducer ให้โครงสร้างที่คาดเดาได้มากกว่าการเรียก useState หลายครั้ง
// Cart management hook with useReducer
import { useReducer, useCallback, useMemo } from 'react'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartState {
items: CartItem[]
isOpen: boolean
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' }
| { type: 'TOGGLE_CART' }
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(
(item) => item.id === action.payload.id
)
if (existingItem) {
// Increment quantity if item exists
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
}
// Add new item
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
}
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter((item) => item.id !== action.payload)
}
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
).filter((item) => item.quantity > 0)
}
case 'CLEAR_CART':
return { ...state, items: [] }
case 'TOGGLE_CART':
return { ...state, isOpen: !state.isOpen }
default:
return state
}
}
const initialState: CartState = {
items: [],
isOpen: false
}
export function useCart() {
const [state, dispatch] = useReducer(cartReducer, initialState)
// Memoized actions to avoid re-renders
const addItem = useCallback(
(item: Omit<CartItem, 'quantity'>) => {
dispatch({ type: 'ADD_ITEM', payload: item })
},
[]
)
const removeItem = useCallback((id: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: id })
}, [])
const updateQuantity = useCallback((id: string, quantity: number) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } })
}, [])
const clearCart = useCallback(() => {
dispatch({ type: 'CLEAR_CART' })
}, [])
const toggleCart = useCallback(() => {
dispatch({ type: 'TOGGLE_CART' })
}, [])
// Memoized derived values
const total = useMemo(
() => state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
),
[state.items]
)
const itemCount = useMemo(
() => state.items.reduce((sum, item) => sum + item.quantity, 0),
[state.items]
)
return {
items: state.items,
isOpen: state.isOpen,
total,
itemCount,
addItem,
removeItem,
updateQuantity,
clearCart,
toggleCart
}
}วิธีนี้รวมศูนย์ตรรกะของตะกร้าทั้งหมดและทำให้เขียน unit test ได้ง่ายขึ้น
พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
การเพิ่มประสิทธิภาพด้วย useMemo และ useCallback
Hook เหล่านี้หลีกเลี่ยงการคำนวณที่ไม่จำเป็นและการสร้างฟังก์ชันใหม่ ควรใช้อย่างมีจุดประสงค์: การใช้ทุกที่จะลดประสิทธิภาพแทนที่จะปรับปรุง
'use client'
import { useState, useMemo, useCallback, memo } from 'react'
interface Product {
id: string
name: string
price: number
category: string
inStock: boolean
}
interface ProductListProps {
products: Product[]
}
// Memoized child component
const ProductCard = memo(function ProductCard({
product,
onAddToCart
}: {
product: Product
onAddToCart: (id: string) => void
}) {
console.log(`Render ProductCard: ${product.name}`)
return (
<div className="border rounded-lg p-4">
<h3 className="font-medium">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
<button
onClick={() => onAddToCart(product.id)}
disabled={!product.inStock}
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{product.inStock ? 'Add to Cart' : 'Out of Stock'}
</button>
</div>
)
})
export function ProductList({ products }: ProductListProps) {
const [filter, setFilter] = useState('')
const [sortBy, setSortBy] = useState<'name' | 'price'>('name')
const [cart, setCart] = useState<string[]>([])
// useMemo: memoize result of expensive computation
const filteredAndSortedProducts = useMemo(() => {
console.log('Computing filteredAndSortedProducts')
let result = products
// Filter by name
if (filter) {
result = result.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
)
}
// Sort
result = [...result].sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name)
}
return a.price - b.price
})
return result
}, [products, filter, sortBy]) // Recompute only when these deps change
// useCallback: memoize a function
const handleAddToCart = useCallback((productId: string) => {
setCart((prev) => [...prev, productId])
}, []) // Empty deps: function never changes
// Memoized statistics
const stats = useMemo(
() => ({
total: filteredAndSortedProducts.length,
inStock: filteredAndSortedProducts.filter((p) => p.inStock).length,
avgPrice:
filteredAndSortedProducts.reduce((sum, p) => sum + p.price, 0) /
filteredAndSortedProducts.length || 0
}),
[filteredAndSortedProducts]
)
return (
<div>
<div className="mb-4 flex gap-4">
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Search..."
className="px-3 py-2 border rounded-lg"
/>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}
className="px-3 py-2 border rounded-lg"
>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
</div>
<p className="text-sm text-gray-600 mb-4">
{stats.total} products ({stats.inStock} in stock) -
Average price: ${stats.avgPrice.toFixed(2)}
</p>
<div className="grid grid-cols-3 gap-4">
{filteredAndSortedProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
</div>
)
}Hook เหล่านี้เพิ่มความซับซ้อน ควรสงวนไว้สำหรับ: (1) การคำนวณที่ใช้ทรัพยากรจริงๆ (2) prop ที่ส่งไปยังคอมโพเนนต์ที่ memo ด้วย memo() (3) dependency ของ hook อื่น
Hook debounce สำหรับช่องค้นหา
Debounce ป้องกันไม่ให้แอ็กชันถูกเรียกบ่อยเกินไป โดยทั่วไปใช้ขณะพิมพ์ในช่องค้นหา
// Generic debounce hook
import { useState, useEffect } from 'react'
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
// Create a timer that updates the value after delay
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
// Cleanup: cancel timer if value changes before delay
return () => {
clearTimeout(timer)
}
}, [value, delay])
return debouncedValue
}'use client'
import { useState, useEffect } from 'react'
import { useDebounce } from '@/hooks/useDebounce'
interface SearchResult {
id: string
title: string
}
export function SearchInput() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [loading, setLoading] = useState(false)
// Debounce the query by 300ms
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
// Don't search if query is empty
if (!debouncedQuery.trim()) {
setResults([])
return
}
async function search() {
setLoading(true)
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(debouncedQuery)}`
)
const data = await response.json()
setResults(data.results)
} catch (error) {
console.error('Search error:', error)
} finally {
setLoading(false)
}
}
search()
}, [debouncedQuery]) // Only triggered when debouncedQuery changes
return (
<div className="relative">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="w-full px-4 py-2 border rounded-lg"
/>
{loading && (
<div className="absolute right-3 top-2.5">
<div className="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
)}
{results.length > 0 && (
<ul className="absolute w-full mt-1 bg-white border rounded-lg shadow-lg">
{results.map((result) => (
<li
key={result.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
>
{result.title}
</li>
))}
</ul>
)}
</div>
)
}คอมโพเนนต์จะค้นหาเฉพาะ 300 มิลลิวินาทีหลังจากที่ผู้ใช้หยุดพิมพ์ ลดคำขอที่ไม่จำเป็นจำนวนมาก
useImperativeHandle สำหรับ ref ขั้นสูง
Hook นี้เปิดเผยเมธอดเฉพาะของคอมโพเนนต์ลูกให้คอมโพเนนต์แม่ผ่าน ref
'use client'
import {
useRef,
useImperativeHandle,
forwardRef,
useState,
useCallback
} from 'react'
// Interface of exposed methods
export interface VideoPlayerRef {
play: () => void
pause: () => void
seek: (time: number) => void
getCurrentTime: () => number
}
interface VideoPlayerProps {
src: string
poster?: string
}
export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
function VideoPlayer({ src, poster }, ref) {
const videoRef = useRef<HTMLVideoElement>(null)
const [isPlaying, setIsPlaying] = useState(false)
// Expose only desired methods to parent
useImperativeHandle(
ref,
() => ({
play: () => {
videoRef.current?.play()
setIsPlaying(true)
},
pause: () => {
videoRef.current?.pause()
setIsPlaying(false)
},
seek: (time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time
}
},
getCurrentTime: () => {
return videoRef.current?.currentTime ?? 0
}
}),
[] // No deps: methods always use current ref
)
const handlePlayPause = useCallback(() => {
if (isPlaying) {
videoRef.current?.pause()
} else {
videoRef.current?.play()
}
setIsPlaying(!isPlaying)
}, [isPlaying])
return (
<div className="relative">
<video
ref={videoRef}
src={src}
poster={poster}
className="w-full rounded-lg"
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
<button
onClick={handlePlayPause}
className="absolute bottom-4 left-4 px-4 py-2 bg-black/50 text-white rounded"
>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
)
}
)'use client'
import { useRef } from 'react'
import { VideoPlayer, VideoPlayerRef } from './VideoPlayer'
export function VideoController() {
const playerRef = useRef<VideoPlayerRef>(null)
const handleSkipForward = () => {
if (playerRef.current) {
const currentTime = playerRef.current.getCurrentTime()
playerRef.current.seek(currentTime + 10)
}
}
return (
<div className="space-y-4">
<VideoPlayer
ref={playerRef}
src="/videos/demo.mp4"
poster="/images/poster.jpg"
/>
<div className="flex gap-2">
<button
onClick={() => playerRef.current?.play()}
className="px-4 py-2 bg-green-600 text-white rounded"
>
Play
</button>
<button
onClick={() => playerRef.current?.pause()}
className="px-4 py-2 bg-red-600 text-white rounded"
>
Pause
</button>
<button
onClick={handleSkipForward}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
+10s
</button>
</div>
</div>
)
}รูปแบบนี้มีประโยชน์อย่างยิ่งสำหรับคอมโพเนนต์มัลติมีเดีย ฟอร์ม หรืออินเทอร์เฟซใดๆ ที่ต้องการการควบคุมแบบสั่งการ
Hook จัดการวงจรชีวิตของ request
Hook ขั้นสูงสำหรับจัดการสถานะ loading, error และข้อมูลด้วยการระบุชนิดข้อมูลที่เข้มงวด
// Generic hook for HTTP requests
import { useState, useEffect, useCallback, useRef } from 'react'
interface UseFetchState<T> {
data: T | null
loading: boolean
error: Error | null
}
interface UseFetchOptions {
immediate?: boolean // Execute immediately on mount
onSuccess?: <T>(data: T) => void
onError?: (error: Error) => void
}
export function useFetch<T>(
url: string,
options: UseFetchOptions = {}
) {
const { immediate = true, onSuccess, onError } = options
const [state, setState] = useState<UseFetchState<T>>({
data: null,
loading: immediate,
error: null
})
// Ref to prevent updates after unmount
const mountedRef = useRef(true)
// Ref for abort controller
const abortControllerRef = useRef<AbortController | null>(null)
const execute = useCallback(async () => {
// Abort previous request if in progress
abortControllerRef.current?.abort()
abortControllerRef.current = new AbortController()
setState((prev) => ({ ...prev, loading: true, error: null }))
try {
const response = await fetch(url, {
signal: abortControllerRef.current.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data: T = await response.json()
if (mountedRef.current) {
setState({ data, loading: false, error: null })
onSuccess?.(data)
}
return data
} catch (error) {
// Ignore abort errors
if (error instanceof Error && error.name === 'AbortError') {
return null
}
const err = error instanceof Error ? error : new Error('Unknown error')
if (mountedRef.current) {
setState({ data: null, loading: false, error: err })
onError?.(err)
}
return null
}
}, [url, onSuccess, onError])
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null })
}, [])
// Execute on mount if immediate=true
useEffect(() => {
if (immediate) {
execute()
}
return () => {
mountedRef.current = false
abortControllerRef.current?.abort()
}
}, [execute, immediate])
return {
...state,
execute,
reset,
isIdle: !state.loading && !state.data && !state.error
}
}Hook นี้มอบ API ที่ครบถ้วนสำหรับจัดการคำขอใดๆ พร้อมการยกเลิกอัตโนมัติและการจัดการวงจรชีวิต
พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
สรุป
รูปแบบ React Hooks ขั้นสูงช่วยให้สามารถสร้างโค้ดที่ดูแลรักษาง่ายและมีประสิทธิภาพสูง ประเด็นสำคัญที่ควรจดจำ:
- ✅ useEffect: ใส่ cleanup เสมอและจัดการ race condition ด้วย AbortController
- ✅ Custom hooks: ห่อหุ้มตรรกะที่นำกลับมาใช้ใหม่ได้ด้วย API ที่ชัดเจน
- ✅ useReducer: เลือกใช้สำหรับ state ที่ซับซ้อนซึ่งมีหลายแอ็กชัน
- ✅ useMemo/useCallback: ใช้อย่างมีจุดประสงค์เฉพาะเมื่อจำเป็น
- ✅ useDebounce: จำกัดการเรียกที่ถี่เกินไปในการค้นหา
- ✅ useImperativeHandle: เปิดเผยเมธอดแบบสั่งการอย่างมีการควบคุม
ความเชี่ยวชาญในรูปแบบเหล่านี้สร้างความแตกต่างระหว่างนักพัฒนา React ระดับกลางกับผู้เชี่ยวชาญ Hook แต่ละตัวแก้ปัญหาเฉพาะ: การเลือกเครื่องมือที่ถูกต้องในเวลาที่ถูกต้องคือกุญแจสู่สถาปัตยกรรม React ที่แข็งแกร่ง
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

React Compiler ในปี 2026: Automatic Memoization และคำถามสัมภาษณ์งาน
เรียนรู้ React Compiler ที่ทำ memoization อัตโนมัติ พร้อมคำถามสัมภาษณ์งานยอดนิยมสำหรับนักพัฒนา React ในปี 2026

React 19: Server Components ในระบบ Production - คู่มือฉบับสมบูรณ์
เชี่ยวชาญ React 19 Server Components ในระบบ Production สถาปัตยกรรม รูปแบบการออกแบบ Streaming Caching และการเพิ่มประสิทธิภาพสำหรับแอปพลิเคชันที่มีประสิทธิภาพสูง

React Server Components ในโปรดักชัน: รูปแบบและกับดัก
React Server Components ในโปรดักชัน: รูปแบบที่ผ่านการพิสูจน์ แอนตี้แพทเทิร์นที่พบบ่อย และกลยุทธ์การดีบักสำหรับแอป Next.js 15 ที่มั่นคง