架构视角:Hooks的设计哲学
React Hooks的引入是React历史上最重要的变革之一。它彻底改变了状态管理和副作用处理的方式,让函数组件拥有了类组件的所有能力,同时保持了更好的可复用性和可测试性。
Hooks核心设计原则
- 组合优于继承:通过自定义Hook组合逻辑,而非高阶组件嵌套
- 声明式副作用:用useEffect等Hook声明副作用,而非生命周期方法
- 闭包捕获:每次渲染都有独立的props和state,避免this指向问题
- 可复用逻辑:自定义Hook让状态逻辑复用变得简单直观
类组件 vs Hooks对比
// 类组件实现
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null,
loading: true,
error: null
};
}
componentDidMount() {
this.fetchUser();
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser();
}
}
componentWillUnmount() {
this.isCancelled = true;
}
async fetchUser() {
this.setState({ loading: true });
try {
const user = await api.getUser(this.props.userId);
if (!this.isCancelled) {
this.setState({ user, loading: false });
}
} catch (error) {
if (!this.isCancelled) {
this.setState({ error, loading: false });
}
}
}
render() {
const { user, loading, error } = this.state;
// 渲染逻辑...
}
}
// Hooks实现
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
try {
const user = await api.getUser(userId);
if (!cancelled) {
setUser(user);
setLoading(false);
}
} catch (error) {
if (!cancelled) {
setError(error);
setLoading(false);
}
}
}
fetchUser();
return () => { cancelled = true; };
}, [userId]);
// 渲染逻辑...
}
核心Hooks深度剖析
useState:状态管理基础
// useState工作原理与最佳实践
// 1. 基础用法
const [count, setCount] = useState(0);
// 2. 函数式更新(避免闭包问题)
setCount(prev => prev + 1);
// 3. 延迟初始化(性能优化)
const [state, setState] = useState(() => {
// 只在初始渲染执行
return expensiveComputation();
});
// 4. 复杂状态管理
// ❌ 多个useState
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [age, setAge] = useState(0);
// ✅ 使用useReducer或合并状态
const [user, setUser] = useState({
firstName: '',
lastName: '',
age: 0
});
// 更新时保持不可变性
setUser(prev => ({ ...prev, age: prev.age + 1 }));
// 5. useState实现原理(简化版)
let hookIndex = 0;
const hooks = [];
function useState(initialValue) {
const state = hooks[hookIndex] !== undefined
? hooks[hookIndex]
: initialValue;
hooks[hookIndex] = state;
const _index = hookIndex;
hookIndex++;
function setState(newValue) {
hooks[_index] = newValue;
reRender(); // 触发重新渲染
}
return [state, setState];
}
useEffect:副作用管理
// useEffect深度理解
// 1. 依赖数组的三种模式
// 每次渲染后执行
useEffect(() => {
console.log('Every render');
});
// 只在挂载时执行
useEffect(() => {
console.log('Mount only');
}, []);
// 依赖变化时执行
useEffect(() => {
console.log('UserId changed:', userId);
}, [userId]);
// 2. 清理函数
useEffect(() => {
const subscription = api.subscribe(userId);
return () => {
// 组件卸载或依赖变化前执行
subscription.unsubscribe();
};
}, [userId]);
// 3. 常见副作用模式
// 数据获取
function useData(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// 订阅
function useSubscription(topic) {
const [message, setMessage] = useState(null);
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/${topic}`);
ws.onmessage = (event) => setMessage(JSON.parse(event.data));
return () => ws.close();
}, [topic]);
return message;
}
// DOM操作
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}
// 4. useEffect执行时机
/*
渲染阶段: render()
提交阶段: React更新DOM
浏览器绘制: 浏览器渲染页面
useEffect执行: 异步执行,不阻塞绘制
useLayoutEffect: 在浏览器绘制前同步执行
*/
// 5. 避免无限循环
// ❌ 错误:setState导致重渲染,形成循环
useEffect(() => {
setCount(count + 1);
}, [count]);
// ✅ 正确:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖,只在挂载时设置
useRef:持久化引用
// useRef使用场景
// 1. 访问DOM元素
function TextInput() {
const inputRef = useRef(null);
const focus = () => {
inputRef.current?.focus();
};
return ;
}
// 2. 保存上一次的值
function usePrevious(value: T): T | undefined {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// 3. 保存不触发渲染的状态
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
// 记住最新的callback
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// 设置interval
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
// 4. 强制重新渲染
function useForceUpdate() {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
}
// 5. 测量DOM
function useMeasure() {
const ref = useRef(null);
const [rect, setRect] = useState();
useEffect(() => {
if (!ref.current) return;
const observer = new ResizeObserver(([entry]) => {
setRect(entry.contentRect);
});
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return { ref, rect };
}
useMemo与useCallback:性能优化
// 记忆化Hooks
// 1. useMemo:缓存计算结果
const expensiveValue = useMemo(() => {
return data.filter(item => item.active)
.map(item => processItem(item))
.sort((a, b) => b.score - a.score);
}, [data]);
// 2. useCallback:缓存函数引用
const handleSubmit = useCallback((values: FormValues) => {
api.submitForm(values).then(onSuccess);
}, [onSuccess]);
// 3. 何时使用?
// ✅ 使用场景:
// - 计算成本高的值
// - 传递给子组件的函数(配合React.memo)
// - 依赖数组用于其他Hooks
// ❌ 避免过度使用:
// - 简单计算
// - 不传递给子组件的函数
// - 过早优化
// 4. 与React.memo配合
const ChildComponent = React.memo(function Child({
data,
onUpdate
}: ChildProps) {
// 只有data或onUpdate变化时才重新渲染
return {/* ... */};
});
function Parent() {
const [count, setCount] = useState(0);
const [data, setData] = useState([]);
// 不使用useCallback,每次渲染都是新函数
// Child会随Parent一起渲染
const badHandleUpdate = () => {
console.log('update');
};
// 使用useCallback,函数引用稳定
// 只有data变化时Child才重新渲染
const goodHandleUpdate = useCallback(() => {
console.log('update', data);
}, [data]);
return (
);
}
// 5. 自定义比较函数
const MemoizedComponent = React.memo(
Component,
(prevProps, nextProps) => {
// 返回true表示不重新渲染
return prevProps.id === nextProps.id;
}
);
高级Hooks与模式
useReducer:复杂状态管理
// useReducer模式
type State = {
loading: boolean;
data: User | null;
error: Error | null;
};
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: User }
| { type: 'FETCH_ERROR'; payload: Error }
| { type: 'RESET' };
const initialState: State = {
loading: false,
data: null,
error: null
};
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
case 'RESET':
return initialState;
default:
return state;
}
}
function UserProfile({ userId }: { userId: string }) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({ type: 'FETCH_START' });
api.getUser(userId)
.then(user => dispatch({ type: 'FETCH_SUCCESS', payload: user }))
.catch(error => dispatch({ type: 'FETCH_ERROR', payload: error }));
}, [userId]);
if (state.loading) return ;
if (state.error) return ;
return ;
}
// 使用immer简化不可变更新
import { useImmerReducer } from 'use-immer';
function reducer(draft: State, action: Action) {
switch (action.type) {
case 'FETCH_START':
draft.loading = true;
draft.error = null;
break;
case 'FETCH_SUCCESS':
draft.loading = false;
draft.data = action.payload;
break;
}
}
useContext:跨组件状态共享
// Context优化模式
// 1. 拆分Context避免不必要的重渲染
// ❌ 一个Context包含所有状态
const AppContext = createContext({
theme: {},
user: {},
settings: {}
});
// ✅ 拆分为独立的Context
const ThemeContext = createContext(null);
const UserContext = createContext(null);
const SettingsContext = createContext(null);
// 2. 自定义Hook封装
function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
}
// 3. Context Selector模式(减少重渲染)
function useUserSelector(selector: (state: UserState) => T): T {
const store = useContext(UserContext);
const [selected, setSelected] = useState(() => selector(store.getState()));
useEffect(() => {
return store.subscribe(() => {
const newSelected = selector(store.getState());
setSelected(prev => {
// 浅比较,避免不必要的更新
return shallowEqual(prev, newSelected) ? prev : newSelected;
});
});
}, [store, selector]);
return selected;
}
// 使用
const userName = useUserSelector(state => state.user.name);
自定义Hooks:逻辑复用
// 实用的自定义Hooks
// 1. 防抖
function useDebounce(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 2. 本地存储同步
function useLocalStorage(key: string, initialValue: T): [T, (val: T) => void] {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// 3. 媒体查询
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);
return matches;
}
// 4. 异步操作管理
function useAsync(asyncFunction: () => Promise) {
const [state, setState] = useState<{
data: T | null;
loading: boolean;
error: Error | null;
}>({
data: null,
loading: false,
error: null
});
const execute = useCallback(async () => {
setState(prev => ({ ...prev, loading: true }));
try {
const data = await asyncFunction();
setState({ data, loading: false, error: null });
return data;
} catch (error) {
setState({ data: null, loading: false, error: error as Error });
throw error;
}
}, [asyncFunction]);
return { ...state, execute };
}
// 5. 无限滚动
function useInfiniteScroll(callback: () => void) {
const observer = useRef();
const lastElementRef = useCallback((node: HTMLElement | null) => {
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
callback();
}
});
if (node) observer.current.observe(node);
}, [callback]);
return lastElementRef;
}
Hooks规则与陷阱
// Hooks使用规则
// 1. 只在最顶层调用Hooks
// ❌ 错误:在条件语句中调用
function BadComponent({ condition }) {
if (condition) {
const [state, setState] = useState(0); // 错误!
}
}
// ✅ 正确
function GoodComponent({ condition }) {
const [state, setState] = useState(0);
useEffect(() => {
if (condition) {
// 在effect中处理条件逻辑
}
}, [condition]);
}
// 2. 只在React函数中调用Hooks
// ✅ 正确的地方:
// - React函数组件
// - 自定义Hooks
// ❌ 错误的地方:
// - 普通JavaScript函数
// - 循环或条件语句中
// - class组件中
// 3. 闭包陷阱
function ClosureTrap() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// ❌ 错误:count永远是0
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖数组
// ✅ 解决方案1:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
// ✅ 解决方案2:使用useRef
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setInterval(() => {
console.log(countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []);
}
// 4. 依赖数组完整性
// ❌ 错误:缺少依赖
useEffect(() => {
console.log(userId);
}, []); // userId变化时不会重新执行
// ✅ 正确:包含所有依赖
useEffect(() => {
console.log(userId);
}, [userId]);
// 使用eslint-plugin-react-hooks自动检查
// .eslintrc
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
常见Hooks陷阱
- ❌ 忘记清理副作用:忘记return清理函数导致内存泄漏
- ❌ 过度使用useMemo/useCallback:增加代码复杂度,收益不明显
- ❌ useEffect依赖数组不完整:导致闭包问题
- ❌ 在useEffect中直接调用async函数:effect不能返回Promise
- ✅ 使用自定义Hook提取重复逻辑:提高代码复用性
架构决策总结
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 简单状态 | useState | 单个或少量相关状态 |
| 复杂状态逻辑 | useReducer | 多状态联动,状态转换复杂 |
| 数据获取 | useEffect + 自定义Hook | 或使用React Query/SWR |
| 跨组件共享状态 | useContext + useReducer | 或使用Zustand/Redux |
| DOM引用 | useRef | 访问DOM或保存可变值 |
| 性能优化 | useMemo/useCallback | 谨慎使用,避免过早优化 |
总结
React Hooks彻底改变了React组件的编写方式。通过理解Hooks的工作原理、遵循使用规则、掌握常见模式,可以编写出更简洁、可复用、易测试的React应用。
Hooks的学习曲线虽然存在,但一旦掌握,将极大提升开发效率和代码质量。记住:Hooks的设计初衷是简化状态逻辑复用,善用自定义Hook,避免过度工程化。