React Hooks 是 React 16.8 引入的革命性特性,它从根本上改变了我们编写 React 组件的方式。作为一名长期使用 React 的开发者,我认为 Hooks 的核心价值在于它让函数组件具备了完整的能力,同时解决了类组件时代诸多痛点。
Hooks 本质上是一组特殊的函数,它们允许你在函数组件中"钩入"React 的状态和生命周期特性。这个设计非常巧妙 - 通过简单的函数调用,就能获得原本只有类组件才具备的能力。
javascript复制// 最简单的 Hook 示例
function Example() {
const [count, setCount] = useState(0);
// ...
}
这里的关键词是"钩入"(Hook) - 这些函数不是创建新的功能,而是让你能够连接到 React 的现有特性中。这种设计保持了 React 的核心概念不变,同时提供了更灵活的使用方式。
在 Hooks 出现之前,React 开发者面临几个主要问题:
状态逻辑难以复用:在类组件中,如果想复用状态逻辑,通常需要使用高阶组件(HOC)或渲染属性(Render Props)模式,这会导致"包装地狱" - 组件嵌套层级过深,代码难以维护。
复杂组件难以理解:随着业务逻辑增长,类组件中的生命周期方法会变得臃肿,相关代码分散在不同的生命周期中,难以追踪和维护。
类组件的学习成本:JavaScript 中的 this 绑定、类继承等概念对新手不够友好,也容易引发各种 bug。
Hooks 通过函数式的解决方案完美解决了这些问题。它让我们能够:
useState 是最基础也是使用频率最高的 Hook,它让函数组件拥有了状态管理能力。
javascript复制const [state, setState] = useState(initialState);
实际开发中,我总结了几个关键使用技巧:
javascript复制// 推荐:初始值通过函数计算
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
javascript复制// 不推荐:可能导致多次渲染
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
};
// 推荐:使用函数式更新
const handleClick = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
javascript复制// 不推荐:合并状态
const [state, setState] = useState({
count: 0,
user: null,
loading: false
});
// 推荐:拆分状态
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect 是处理副作用的强大工具,它统一了类组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 生命周期。
javascript复制useEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑
};
}, [dependencies]);
在实际项目中,我总结了以下经验:
依赖数组的精确控制:
[]:仅在组件挂载时运行一次[a, b]:当 a 或 b 变化时运行异步操作的处理:
javascript复制useEffect(() => {
let isMounted = true;
async function fetchData() {
const result = await someAsyncOperation();
if (isMounted) {
setData(result);
}
}
fetchData();
return () => {
isMounted = false;
};
}, []);
性能优化:对于昂贵的副作用,可以使用 useMemo 或 useCallback 配合优化。
useRef 主要有两个用途:
javascript复制const refContainer = useRef(initialValue);
实际应用场景:
DOM 操作:
javascript复制function TextInput() {
const inputEl = useRef(null);
const focusInput = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={focusInput}>Focus</button>
</>
);
}
保存前一次状态:
javascript复制function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return (
<div>
<p>Now: {count}, Before: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
useContext 让我们能够轻松访问 React 的 Context,避免了 props 层层传递的问题。
javascript复制const value = useContext(MyContext);
典型使用模式:
创建 Context:
javascript复制const ThemeContext = React.createContext('light');
提供 Context 值:
javascript复制function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
消费 Context 值:
javascript复制function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}
在实际项目中,我通常会将 Context 与 useReducer 结合使用,创建轻量级的全局状态管理方案。
useMemo 用于缓存计算结果,避免在每次渲染时都进行昂贵的计算。
javascript复制const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
使用场景:
复杂计算优化:
javascript复制function ExpensiveComponent({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(item => item.includes(filter));
}, [items, filter]);
return <List items={filteredItems} />;
}
避免不必要的子组件渲染:
javascript复制function Parent({ a, b }) {
const childProps = useMemo(() => ({ a, b }), [a, b]);
return <Child {...childProps} />;
}
注意:useMemo 不应被滥用,只有在确实有性能问题时才使用它。对于简单的计算,直接计算可能比 useMemo 更高效。
useCallback 返回一个记忆化的回调函数,只有当依赖项变化时才会更新。
javascript复制const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
典型使用场景:
优化子组件渲染:
javascript复制function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return <Child onClick={handleClick} />;
}
const Child = React.memo(function Child({ onClick }) {
return <button onClick={onClick}>Click me</button>;
});
作为其他 Hook 的依赖:
javascript复制useEffect(() => {
fetchData(memoizedCallback);
}, [memoizedCallback]);
useReducer 是 useState 的替代方案,适用于复杂的状态逻辑。
javascript复制const [state, dispatch] = useReducer(reducer, initialArg, init);
使用模式:
定义 reducer:
javascript复制function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
在组件中使用:
javascript复制function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}
对于大型应用,可以将 useReducer 与 useContext 结合,创建类似 Redux 的状态管理方案。
只在最顶层使用 Hooks:
只在 React 函数中调用 Hooks:
违反这些规则会导致 bug 和不可预测的行为。React 提供了 ESLint 插件来强制执行这些规则。
自定义 Hook 是复用状态逻辑的强大工具。一个好的自定义 Hook 应该:
use 开头命名(这是约定)示例:封装数据获取逻辑
javascript复制function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, error, loading };
}
使用:
javascript复制function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
}
依赖项优化:
避免不必要的 effect 执行:
javascript复制useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
}, [props.source]); // 只有当 props.source 改变时才重新订阅
使用 React.memo 优化组件:
javascript复制const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});
常见于 useEffect 中不正确的依赖项设置:
javascript复制// 错误:会导致无限循环
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, [count]); // 每次 count 变化都会触发 effect
解决方案:
javascript复制useEffect(() => {
setCount(c => c + 1);
}, []); // 空依赖数组
在异步操作中可能会遇到闭包捕获旧值的问题:
javascript复制function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 总是打印初始值
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖数组
return <div>{count}</div>;
}
解决方案:
错误示例:
javascript复制if (condition) {
useEffect(() => {
// ...
});
}
正确做法:
javascript复制useEffect(() => {
if (condition) {
// ...
}
});
React 会自动批处理同步的状态更新,但在异步操作中需要注意:
javascript复制// 同步:批处理
const handleClick = () => {
setCount(c => c + 1);
setName('new name');
}; // 一次渲染
// 异步:可能不会批处理
const handleClickAsync = async () => {
await something();
setCount(c => c + 1);
setName('new name');
}; // 可能两次渲染
解决方案:使用 React 18 的自动批处理或手动合并状态更新。
| 类组件生命周期 | Hooks 等效实现 |
|---|---|
| constructor | useState 初始化 |
| componentDidMount | useEffect 空依赖数组 |
| componentDidUpdate | useEffect 带依赖数组 |
| componentWillUnmount | useEffect 的清理函数 |
| shouldComponentUpdate | React.memo 或 useMemo |
| getDerivedStateFromProps | 在渲染时更新状态 |
| getSnapshotBeforeUpdate | 目前没有直接等效 |
类组件:
javascript复制class Example extends React.Component {
constructor(props) {
super(props);
this.state = { /* ... */ };
// 绑定方法
}
componentDidMount() { /* ... */ }
componentDidUpdate() { /* ... */ }
componentWillUnmount() { /* ... */ }
// 相关逻辑分散在不同方法中
render() { /* ... */ }
}
函数组件 + Hooks:
javascript复制function Example() {
// 状态
const [state, setState] = useState();
// 副作用
useEffect(() => {
// 挂载逻辑
return () => {
// 卸载逻辑
};
}, []);
// 相关逻辑组织在一起
return /* ... */;
}
在实际项目中,我观察到使用 Hooks 的组件通常更易于维护和测试,代码量也更少。但对于非常复杂的组件,类组件有时可能更直观。
javascript复制// store.js
export const StoreContext = React.createContext();
export const StoreProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StoreContext.Provider value={{ state, dispatch }}>
{children}
</StoreContext.Provider>
);
};
// 在组件中使用
function Component() {
const { state, dispatch } = useContext(StoreContext);
// ...
}
javascript复制function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
// 其他方法
}));
return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);
// 使用
function Parent() {
const inputRef = useRef();
const handleClick = () => {
inputRef.current.focus();
};
return (
<>
<FancyInput ref={inputRef} />
<button onClick={handleClick}>Focus</button>
</>
);
}
javascript复制function Tooltip() {
const ref = useRef();
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
// 在浏览器绘制前测量
setTooltipHeight(ref.current.offsetHeight);
}, []);
// 使用测量结果渲染
}
javascript复制function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
function useResponsiveLayout() {
const { width } = useWindowSize();
const [layout, setLayout] = useState('desktop');
useEffect(() => {
if (width < 768) {
setLayout('mobile');
} else if (width < 1024) {
setLayout('tablet');
} else {
setLayout('desktop');
}
}, [width]);
return layout;
}
React 团队持续改进 Hooks 的实现和性能。一些值得关注的趋势:
在实际开发中,我建议保持对新特性的关注,但不要过早采用实验性 API。稳定性和可维护性应该是首要考虑因素。