React Hooks 最佳实践与性能优化
(7 个月前)
Nick Chen
约 5 分钟
React Hooks 已经成为现代 React 开发的标准,但正确使用它们需要深入理解其工作原理。本文将分享一些高级技巧和最佳实践。
基础 Hooks 深度解析
useState 的高级用法
// 函数式更新,避免闭包陷阱
const [count, setCount] = useState(0);
// 不好的做法
const increment = () => {
setTimeout(() => {
setCount(count + 1); // 可能使用过期的 count 值
}, 1000);
};
// 好的做法
const increment = () => {
setTimeout(() => {
setCount(prevCount => prevCount + 1); // 总是使用最新值
}, 1000);
};
// 惰性初始化,避免重复计算
const [expensiveValue, setExpensiveValue] = useState(() => {
return computeExpensiveValue();
});
useEffect 的细节处理
// 清理函数的重要性
useEffect(() => {
const subscription = subscribe()
return () => {
subscription.unsubscribe()
}
}, [])
// 依赖数组的优化
const [userId, setUserId] = useState(null)
const [userData, setUserData] = useState(null)
useEffect(() => {
if (!userId)
return
let cancelled = false
async function fetchUser() {
try {
const data = await api.getUser(userId)
if (!cancelled) {
setUserData(data)
}
}
catch (error) {
if (!cancelled) {
console.error('Failed to fetch user:', error)
}
}
}
fetchUser()
return () => {
cancelled = true
}
}, [userId])
自定义 Hooks 设计模式
数据获取 Hook
function useAsync(asyncFunction, dependencies = []) {
const [state, setState] = useState({
data: null,
loading: true,
error: null
})
useEffect(() => {
let cancelled = false
setState(prev => ({ ...prev, loading: true, error: null }))
asyncFunction()
.then((data) => {
if (!cancelled) {
setState({ data, loading: false, error: null })
}
})
.catch((error) => {
if (!cancelled) {
setState({ data: null, loading: false, error })
}
})
return () => {
cancelled = true
}
}, dependencies)
return state
}
// 使用示例
function UserProfile({ userId }) {
const { data: user, loading, error } = useAsync(
() => api.getUser(userId),
[userId]
)
if (loading)
return <div>Loading...</div>
if (error) {
return (
<div>
Error:
{error.message}
</div>
)
}
return (
<div>
Hello,
{user?.name}
!
</div>
)
}
本地存储 Hook
function useLocalStorage(key, initialValue) {
// 使用函数来避免在每次渲染时访问 localStorage
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 {
// 允许 value 是一个函数,类似于 useState
const valueToStore = typeof value === '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 Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme:
{' '}
{theme}
</button>
)
}
防抖 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, 300)
const { data: results, loading } = useAsync(
() => debouncedSearchTerm ? api.search(debouncedSearchTerm) : Promise.resolve([]),
[debouncedSearchTerm]
)
return (
<div>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
{loading && <div>Searching...</div>}
<ul>
{results?.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
)
}
性能优化技巧
useMemo 和 useCallback 的正确使用
function ExpensiveComponent({ items, onItemClick, filter }) {
// 缓存昂贵的计算
const filteredItems = useMemo(() => {
return items.filter(item => item.category === filter)
}, [items, filter])
// 缓存事件处理函数,避免子组件不必要的重渲染
const handleItemClick = useCallback((itemId) => {
onItemClick(itemId, filter)
}, [onItemClick, filter])
return (
<div>
{filteredItems.map(item => (
<ItemComponent
key={item.id}
item={item}
onClick={handleItemClick}
/>
))}
</div>
)
}
// 使用 memo 包装子组件
const ItemComponent = React.memo(({ item, onClick }) => {
return (
<div onClick={() => onClick(item.id)}>
{item.name}
</div>
)
})
状态结构优化
// 不好的做法:频繁的状态更新
function BadCounter() {
const [count, setCount] = useState(0)
const [lastUpdated, setLastUpdated] = useState(Date.now())
const increment = () => {
setCount(c => c + 1)
setLastUpdated(Date.now()) // 两次 setState 调用
}
return (
<div>
Count:
{count}
{' '}
(Updated:
{lastUpdated}
)
</div>
)
}
// 好的做法:合并相关状态
function GoodCounter() {
const [state, setState] = useState({
count: 0,
lastUpdated: Date.now()
})
const increment = () => {
setState(prev => ({
count: prev.count + 1,
lastUpdated: Date.now()
}))
}
return (
<div>
Count:
{state.count}
{' '}
(Updated:
{state.lastUpdated}
)
</div>
)
}
useReducer 用于复杂状态管理
const initialState = {
items: [],
loading: false,
error: null,
filter: 'all'
}
function todosReducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null }
case 'FETCH_SUCCESS':
return { ...state, loading: false, items: action.payload }
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload }
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] }
case 'TOGGLE_ITEM':
return {
...state,
items: state.items.map(item =>
item.id === action.payload
? { ...item, completed: !item.completed }
: item
)
}
case 'SET_FILTER':
return { ...state, filter: action.payload }
default:
return state
}
}
function TodoApp() {
const [state, dispatch] = useReducer(todosReducer, initialState)
const addTodo = useCallback((text) => {
dispatch({
type: 'ADD_ITEM',
payload: { id: Date.now(), text, completed: false }
})
}, [])
const toggleTodo = useCallback((id) => {
dispatch({ type: 'TOGGLE_ITEM', payload: id })
}, [])
// 渲染逻辑...
}
常见陷阱和解决方案
闭包陷阱
// 问题:useEffect 中的闭包陷阱
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // count 永远是 0
}, 1000);
return () => clearInterval(interval);
}, []); // 空依赖数组导致 count 被"困住"
return <div>{count}</div>;
}
// 解决方案 1:使用函数式更新
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1); // 使用最新的 count
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}
// 解决方案 2:使用 useRef
function Timer() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const interval = setInterval(() => {
setCount(countRef.current + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}
依赖数组优化
// 问题:对象和函数作为依赖
function UserProfile({ userId, config }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId, config).then(setUser);
}, [userId, config]); // config 对象可能每次都是新的
// ...
}
// 解决方案:使用 useMemo 和 useCallback
function UserProfile({ userId, config }) {
const [user, setUser] = useState(null);
const stableConfig = useMemo(() => config, [
config.apiUrl,
config.timeout,
// 只依赖真正变化的属性
]);
useEffect(() => {
fetchUser(userId, stableConfig).then(setUser);
}, [userId, stableConfig]);
// ...
}
测试 Hooks
import { act, renderHook } from '@testing-library/react'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('should increment counter', () => {
const { result } = renderHook(() => useCounter(0))
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
it('should reset counter', () => {
const { result } = renderHook(() => useCounter(5))
act(() => {
result.current.reset()
})
expect(result.current.count).toBe(0)
})
})
总结
React Hooks 为我们提供了强大而灵活的状态管理和副作用处理能力。通过遵循最佳实践,我们可以:
- 避免常见陷阱:正确处理依赖数组和闭包问题
- 优化性能:合理使用 memoization 和状态结构
- 提高复用性:设计高质量的自定义 Hooks
- 增强可测试性:编写清晰、可测试的 Hook 逻辑
记住,Hooks 的力量在于组合,通过组合小而专注的 Hooks,我们可以构建出强大而maintainable的应用程序。
参考资源: