13. 自定义 Hook 如何编写?有哪些最佳实践?
答案:
自定义 Hook 的基本概念: 自定义 Hook 是一个以 "use" 开头的 JavaScript 函数,可以调用其他 Hook。
基本结构:
function useCustomHook(initialValue) {
// 可以调用其他 Hook
const [state, setState] = useState(initialValue)
// 返回状态和函数
return [state, setState]
}
常见自定义 Hook 示例:
1. useCounter - 计数器 Hook
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue)
const increment = useCallback(() => {
setCount((prev) => prev + 1)
}, [])
const decrement = useCallback(() => {
setCount((prev) => prev - 1)
}, [])
const reset = useCallback(() => {
setCount(initialValue)
}, [initialValue])
return {
count,
increment,
decrement,
reset,
}
}
// 使用示例
function CounterComponent() {
const { count, increment, decrement, reset } = useCounter(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
)
}
2. useLocalStorage - 本地存储 Hook
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
const setValue = useCallback(
(value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
},
[key, storedValue]
)
return [storedValue, setValue]
}
// 使用示例
function SettingsComponent() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
const [language, setLanguage] = useLocalStorage('language', 'en')
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</div>
)
}
3. useFetch - 数据获取 Hook
function useFetch(url, options = {}) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
const fetchData = async () => {
try {
setLoading(true)
setError(null)
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
if (!cancelled) {
setData(result)
}
} catch (err) {
if (!cancelled) {
setError(err.message)
}
} finally {
if (!cancelled) {
setLoading(false)
}
}
}
fetchData()
return () => {
cancelled = true
}
}, [url, JSON.stringify(options)])
return { data, loading, error }
}
// 使用示例
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`)
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!user) return <div>No user found</div>
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
4. useDebounce - 防抖 Hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
// 使用示例
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('')
const debouncedSearchTerm = useDebounce(searchTerm, 500)
const [results, setResults] = useState([])
useEffect(() => {
if (debouncedSearchTerm) {
searchAPI(debouncedSearchTerm).then(setResults)
} else {
setResults([])
}
}, [debouncedSearchTerm])
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
{results.map((result) => (
<div key={result.id}>{result.title}</div>
))}
</div>
)
}
5. useToggle - 开关 Hook
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => {
setValue((prev) => !prev)
}, [])
const setTrue = useCallback(() => {
setValue(true)
}, [])
const setFalse = useCallback(() => {
setValue(false)
}, [])
return [value, { toggle, setTrue, setFalse }]
}
// 使用示例
function ModalComponent() {
const [isOpen, { toggle, setTrue, setFalse }] = useToggle(false)
return (
<div>
<button onClick={setTrue}>Open Modal</button>
{isOpen && (
<div className="modal">
<h2>Modal Title</h2>
<button onClick={setFalse}>Close</button>
</div>
)}
</div>
)
}
6. usePrevious - 获取前一次值 Hook
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
// 使用示例
function CounterWithPrevious() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
7. useWindowSize - 窗口尺寸 Hook
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
})
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return windowSize
}
// 使用示例
function ResponsiveComponent() {
const { width, height } = useWindowSize()
return (
<div>
<p>
Window size: {width} x {height}
</p>
{width < 768 ? <MobileView /> : <DesktopView />}
</div>
)
}
8. useAsync - 异步操作 Hook
function useAsync(asyncFunction, dependencies = []) {
const [state, setState] = useState({
data: null,
loading: false,
error: null,
})
const execute = useCallback(async (...args) => {
setState((prev) => ({ ...prev, loading: true, error: null }))
try {
const data = await asyncFunction(...args)
setState({ data, loading: false, error: null })
return data
} catch (error) {
setState({ data: null, loading: false, error: error.message })
throw error
}
}, dependencies)
return { ...state, execute }
}
// 使用示例
function DataComponent() {
const { data, loading, error, execute } = useAsync(fetchUserData)
useEffect(() => {
execute(123) // 获取用户ID为123的数据
}, [execute])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return <div>{data?.name}</div>
}
最佳实践:
1. 命名规范
// ✅ 正确:以 use 开头
function useCounter() {}
function useLocalStorage() {}
function useFetch() {}
// ❌ 错误:不以 use 开头
function counter() {}
function localStorage() {}
function fetch() {}
2. 单一职责
// ✅ 正确:单一职责
function useCounter() {
// 只处理计数器逻辑
}
function useLocalStorage() {
// 只处理本地存储逻辑
}
// ❌ 错误:职责过多
function useEverything() {
// 处理计数器、本地存储、网络请求等
}
3. 返回对象而不是数组
// ✅ 推荐:返回对象,更清晰
function useCounter() {
const [count, setCount] = useState(0)
return {
count,
increment: () => setCount((prev) => prev + 1),
decrement: () => setCount((prev) => prev - 1),
}
}
// 使用
const { count, increment, decrement } = useCounter()
// 也可以返回数组,但对象更清晰
function useCounter() {
const [count, setCount] = useState(0)
return [count, setCount]
}
4. 处理边界情况
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
// 处理 JSON 解析错误
console.error(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
const setValue = useCallback(
(value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
// 处理存储错误
console.error(`Error setting localStorage key "${key}":`, error)
}
},
[key, storedValue]
)
return [storedValue, setValue]
}
5. 使用 TypeScript
interface UseCounterReturn {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
function useCounter(initialValue: number = 0): UseCounterReturn {
const [count, setCount] = useState(initialValue)
const increment = useCallback(() => {
setCount((prev) => prev + 1)
}, [])
const decrement = useCallback(() => {
setCount((prev) => prev - 1)
}, [])
const reset = useCallback(() => {
setCount(initialValue)
}, [initialValue])
return { count, increment, decrement, reset }
}
6. 测试自定义 Hook
import { renderHook, act } from '@testing-library/react-hooks'
import { useCounter } from './useCounter'
test('should increment counter', () => {
const { result } = renderHook(() => useCounter(0))
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
7. 文档和注释
/**
* 自定义计数器 Hook
* @param {number} initialValue - 初始值,默认为 0
* @returns {Object} 包含 count、increment、decrement、reset 的对象
* @example
* const { count, increment, decrement, reset } = useCounter(10)
*/
function useCounter(initialValue = 0) {
// 实现...
}