React Native:一文搞定React Hook 函数进阶
一、Hook 函数概述
1.1 Hook 定义
Hook 是 React 16.8 版本引入的新特性,本质是一套可以在函数组件中“钩入”React 内部状态和生命周期等特性的函数,无需编写 class 组件就能使用状态管理、副作用处理等核心能力,解决了 class 组件中逻辑复用复杂(如 HOC、Render Props 嵌套问题)、生命周期函数内逻辑混乱等痛点。
1.2 Hook 核心设计目标
- 简化逻辑复用:替代高阶组件(HOC)和 Render Props,通过自定义 Hook 直接抽取和复用组件逻辑,避免组件嵌套层级过深。
- 优化组件结构:将分散在不同生命周期的相关逻辑(如“数据请求-加载状态-错误处理”)聚合到同一 Hook 中,提升代码可读性。
- 降低学习成本:无需理解 class 组件的
this绑定、生命周期划分等复杂概念,用函数式思维开发 React 组件。
1.3 Hook 使用限制
Hook 需遵循严格规则,否则会破坏 React 对 Hook 调用顺序的追踪:
- 只能在函数组件的顶层调用 Hook,不能在条件语句、循环、嵌套函数中调用(如
if (flag) { useState() }是错误写法)。 - 只能在React 函数组件或自定义 Hook 中调用 Hook,不能在普通 JavaScript 函数中调用(如
function handleClick() { useEffect() }是错误写法)。
二、常用内置 Hook 详解
React 提供了多个内置 Hook,覆盖状态管理、副作用处理、上下文访问等核心场景,以下是最常用的 6 个内置 Hook:
2.1 useState:状态管理 Hook
2.1.1 基本用法
useState 用于在函数组件中创建局部状态,返回一个“状态值 + 更新状态的函数”组成的数组,语法如下:
const [state, setState] = useState(initialState);
initialState:状态初始值,支持任意数据类型(数字、字符串、对象、数组等);若初始值计算昂贵(如复杂计算、数据转换),可传入函数(仅初始化时执行一次),例如useState(() => computeExpensiveValue())。state:当前状态值,每次组件渲染时获取最新状态。setState:状态更新函数,接收两种参数:- 直接传入新值(如
setState(10)):适用于状态与前值无关的场景。 - 传入更新函数(如
setState(prevState => prevState + 1)):适用于状态更新依赖前值的场景(如计数器、累加逻辑),避免因 React 批量更新导致的状态覆盖问题。
- 直接传入新值(如
2.1.2 注意事项
setState是异步更新(在合成事件和生命周期中),若需在状态更新后获取最新值,需通过useEffect监听状态变化。- 状态更新是替换而非合并:若状态是对象(如
{ name: 'A', age: 18 }),更新时需手动合并旧属性(如setState(prev => ({ ...prev, age: 19 }))),否则会丢失未更新的属性(class 组件的setState会自动合并对象状态)。
2.2 useEffect:副作用处理 Hook
2.2.1 基本概念
“副作用”指组件渲染之外的操作,如数据请求、DOM 操作、事件监听、定时器等。useEffect 用于在函数组件中处理副作用,等效于 class 组件的 componentDidMount(挂载后)、componentDidUpdate(更新后)、componentWillUnmount(卸载前)三个生命周期的组合。
2.2.2 基本用法
useEffect(effectCallback, dependencies);
effectCallback:副作用执行函数,组件渲染后(首次渲染 + 更新渲染)会调用该函数;若函数返回一个清理函数,则在组件卸载前或下次effectCallback执行前调用,用于清除副作用(如取消定时器、移除事件监听)。dependencies:依赖数组,控制effectCallback的执行时机:- 不传依赖数组:组件每次渲染(首次 + 所有更新)后都执行
effectCallback。 - 传空数组
[]:仅在组件首次渲染(挂载)后执行effectCallback,清理函数仅在组件卸载前执行(等效于componentDidMount+componentWillUnmount)。 - 传特定依赖(如
[count, user]):仅当依赖数组中的值发生变化时,才执行effectCallback(等效于componentDidMount+ 监听依赖变化的componentDidUpdate)。
- 不传依赖数组:组件每次渲染(首次 + 所有更新)后都执行
2.2.3 常见场景示例
- 数据请求(挂载时请求一次,依赖变化时重新请求):
useEffect(() => {
const fetchData = async () => {
const res = await fetch(`/api/data?userId=${userId}`);
const data = await res.json();
setData(data);
};
fetchData();
}, [userId]); // 仅当 userId 变化时重新请求
- DOM 操作(监听窗口大小变化):
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// 清理函数:卸载前移除事件监听
return () => window.removeEventListener('resize', handleResize);
}, []); // 仅挂载时添加监听,卸载时移除
2.3 useContext:上下文访问 Hook
2.3.1 基本作用
useContext 用于在函数组件中直接访问 React Context 中的数据,无需通过 Context.Consumer 嵌套组件,简化上下文数据的传递和使用。
2.3.2 使用步骤
- 先通过
React.createContext创建 Context 对象:
const ThemeContext = React.createContext('light'); // 'light' 是默认值(仅当无 Provider 时生效)
- 在组件树顶层用
ThemeContext.Provider提供数据:
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Navbar /> {/* 子组件可访问 ThemeContext */}
</ThemeContext.Provider>
);
}
- 在子组件中用
useContext访问数据:
function Navbar() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<div style={{ background: theme === 'light' ? 'white' : 'black' }}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</div>
);
}
2.3.3 注意事项
useContext接收的是 Context 对象本身(如ThemeContext),而非ThemeContext.Provider或ThemeContext.Consumer。- 当
Context.Provider的value变化时,所有使用useContext访问该 Context 的组件都会重新渲染。
2.4 useReducer:复杂状态管理 Hook
2.4.1 基本作用
当组件状态逻辑复杂(如状态包含多个子值、状态更新依赖多个条件)时,useReducer 比 useState 更适合——它通过“ reducer 函数”统一管理状态更新逻辑,类似 Redux 的核心思想,让状态变化更可预测、更易调试。
2.4.2 基本用法
const [state, dispatch] = useReducer(reducer, initialState);
reducer:状态更新函数,接收两个参数(state, action),返回新状态:state:当前状态。action:描述状态变化的“动作”,通常是{ type: '动作类型', payload: '附加数据' }格式。
initialState:状态初始值。state:当前状态值。dispatch:触发状态更新的函数,通过传入action调用reducer,例如dispatch({ type: 'INCREMENT', payload: 1 })。
2.4.3 示例:计数器(复杂状态场景)
// 1. 定义 reducer 函数
function countReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + action.payload };
case 'DECREMENT':
return { ...state, count: state.count - action.payload };
case 'RESET':
return { ...state, count: 0 };
default:
throw new Error('未知 action 类型');
}
}
// 2. 在组件中使用 useReducer
function Counter() {
const initialState = { count: 0, name: '计数器' };
const [state, dispatch] = useReducer(countReducer, initialState);
return (
<div>
<h2>{state.name}: {state.count}</h2>
<button onClick={() => dispatch({ type: 'INCREMENT', payload: 1 })}>+1</button>
<button onClick={() => dispatch({ type: 'DECREMENT', payload: 1 })}>-1</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
</div>
);
}
2.5 useRef:引用 Hook
2.5.1 基本作用
useRef 用于创建一个可变的 ref 对象,该对象的 current 属性可存储任意值(如 DOM 元素、定时器 ID、组件实例等),且 ref 对象的变化不会触发组件重新渲染,解决了“在函数组件中保存跨渲染周期的可变值”的需求。
2.5.2 两大核心场景
- 访问 DOM 元素(替代 class 组件的
this.refs):
function InputComponent() {
const inputRef = useRef(null); // 初始值为 null
const focusInput = () => {
inputRef.current.focus(); // 通过 ref.current 访问 DOM 元素
};
return (
<div>
<input type="text" ref={inputRef} /> {/* 将 DOM 元素绑定到 ref */}
<button onClick={focusInput}>聚焦输入框</button>
</div>
);
}
- 保存跨渲染的可变值(如定时器 ID、前一次状态):
function TimerComponent() {
const [count, setCount] = useState(0);
const timerRef = useRef(null); // 存储定时器 ID
useEffect(() => {
timerRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// 清理函数:清除定时器
return () => clearInterval(timerRef.current);
}, []);
const stopTimer = () => {
clearInterval(timerRef.current); // 访问跨渲染保存的定时器 ID
};
return (
<div>
<p>计时: {count}s</p>
<button onClick={stopTimer}>停止计时</button>
</div>
);
}
2.5.3 注意事项
ref.current的变化不会触发组件重新渲染,若需在ref.current变化时执行逻辑,需配合useEffect监听。
2.6 useMemo & useCallback:性能优化 Hook
两者均用于避免组件不必要的重新计算或重新渲染,核心是“缓存计算结果或函数引用”,仅在依赖变化时更新。
2.6.1 useMemo:缓存计算结果
- 作用:当组件中有昂贵的计算逻辑(如复杂数组排序、数据转换)时,
useMemo可缓存计算结果,避免每次渲染都重复执行计算。 - 语法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); - 逻辑:仅当依赖数组
[a, b]中的值变化时,才重新执行computeExpensiveValue并更新缓存;否则直接返回缓存的结果。
2.6.2 useCallback:缓存函数引用
- 作用:当组件将函数作为 props 传递给子组件时,
useCallback可缓存函数引用,避免每次父组件渲染都创建新函数,导致子组件(尤其是使用React.memo优化的子组件)不必要的重新渲染。 - 语法:
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); - 逻辑:仅当依赖数组
[a, b]中的值变化时,才创建新函数并更新缓存;否则直接返回缓存的函数引用。
2.6.3 示例:父子组件性能优化
// 子组件:用 React.memo 包裹,仅当 props 变化时重新渲染
const Child = React.memo(({ onClick, data }) => {
console.log('Child 重新渲染');
return <button onClick={onClick}>{data}</button>;
});
// 父组件:用 useCallback 缓存函数,用 useMemo 缓存计算结果
function Parent() {
const [count, setCount] = useState(0);
// 缓存函数:仅当 count 变化时,函数引用才更新
const handleClick = useCallback(() => {
console.log('点击事件,count:', count);
}, [count]);
// 缓存计算结果:仅当 count 变化时,才重新计算 sortedData
const sortedData = useMemo(() => {
return [1, 3, 2].sort((a, b) => a - b + count); // 模拟昂贵计算
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(prev => prev + 1)}>增加 count</button>
<Child onClick={handleClick} data={sortedData} />
</div>
);
}
三、自定义 Hook 开发
3.1 自定义 Hook 定义与规则
自定义 Hook 是遵循 Hook 规则的普通 JavaScript 函数,名称必须以 use 开头(如 useFetch、useTimer),内部可调用其他内置 Hook 或自定义 Hook,目的是抽取和复用组件间的通用逻辑。
3.1.1 核心规则
- 名称必须以
use开头:React 依赖该命名规则识别 Hook,确保 ESLint 插件(如eslint-plugin-react-hooks)能检查 Hook 使用是否合规。 - 内部可调用其他 Hook:如在自定义 Hook 中使用
useState、useEffect等,但需遵循 Hook 的使用限制(不能在条件、循环中调用)。
3.2 自定义 Hook 示例
示例 1:useFetch(数据请求 Hook)
抽取“数据请求-加载状态-错误处理”的通用逻辑:
function useFetch(url, dependencies = []) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 重置状态
setLoading(true);
setError(null);
const fetchData = async () => {
try {
const res = await fetch(url);
if (!res.ok) throw new Error('请求失败');
const result = await res.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, ...dependencies]); // 依赖 url 和外部传入的 dependencies
// 返回请求相关状态和数据
return { data, loading, error };
}
// 使用自定义 Hook
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
示例 2:useLocalStorage(本地存储 Hook)
抽取“读取/写入 localStorage”的逻辑,实现状态与本地存储同步:
function useLocalStorage(key, initialValue) {
// 从 localStorage 读取初始值(优先级高于 initialValue)
const [value, setValue] = useState(() => {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : initialValue;
});
// 当 value 变化时,同步写入 localStorage
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
// 返回状态和更新函数
return [value, setValue];
}
// 使用自定义 Hook
function ThemeSetting() {
// 状态与 localStorage 的 'theme' 键同步
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div>
<p>当前主题: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</div>
);
}
四、Hook 高级特性与最佳实践
4.1 Hook 调用顺序的重要性
React 依赖每次组件渲染时 Hook 调用顺序的一致性来追踪状态(如 useState 的第 1 次调用对应第 1 个状态,第 2 次调用对应第 2 个状态)。若在条件语句中调用 Hook,会导致每次渲染时 Hook 调用顺序变化,进而引发状态错乱或报错。
错误示例(条件中调用 Hook):
function BadComponent() {
const [count, setCount] = useState(0);
if (count > 5) {
// 错误:Hook 不能在条件中调用,当 count > 5 时会多调用一次 useEffect
useEffect(() => {
console.log('Count 超过 5');
}, []);
}
return <button onClick={() => setCount(prev => prev + 1)}>{count}</button>;
}
正确示例(在 Hook 内部处理条件):
function GoodComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 正确:在 Hook 内部判断条件,保证 Hook 调用顺序一致
if (count > 5) {
console.log('Count 超过 5');
}
}, [count]);
return <button onClick={() => setCount(prev => prev + 1)}>{count}</button>;
}
4.2 避免过度使用 useMemo/useCallback
useMemo和useCallback本身有性能开销(需存储缓存、对比依赖),若计算逻辑简单(如a + b)或子组件无需优化(未用React.memo),过度使用会适得其反。- 建议仅在以下场景使用:
useMemo:处理昂贵的计算逻辑(如耗时超过 1ms 的操作)。useCallback:函数作为 props 传递给用 React.memo 优化的子组件,或函数作为依赖传入其他 Hook(如useEffect)。
4.3 拆分复杂 useEffect
若一个 useEffect 中包含多个不相关的副作用(如“数据请求”和“DOM 监听”),会导致逻辑混乱、依赖数组臃肿。建议按“功能相关性”拆分多个 useEffect,每个 useEffect 只处理一类副作用。
错误示例(一个 useEffect 处理多个副作用):
useEffect(() => {
// 副作用 1:数据请求
const fetchData = async () => {
const res = await fetch('/api/data');
const data = await res.json();
setData(data);
};
fetchData();
// 副作用 2:DOM 监听
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// 清理函数:混合两种副作用的清理逻辑
return () => {
window.removeEventListener('resize', handleResize);
};
}, [userId]); // 依赖仅与数据请求相关,与 DOM 监听无关
正确示例(拆分多个 useEffect):
// 副作用 1:数据请求(依赖 userId)
useEffect(() => {
const fetchData = async () => {
const res = await fetch(`/api/data?userId=${userId}`);
const data = await res.json();
setData(data);
};
fetchData();
}, [userId]);
// 副作用 2:DOM 监听(无依赖)
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
4.4 自定义 Hook 的复用边界
自定义 Hook 应聚焦于“通用逻辑”,避免包含与特定业务强耦合的代码(如特定 API 地址、业务常量)。可通过参数将业务相关配置(如 url、headers)传入,提升 Hook 的复用性。
五、Hook 与 Class 组件的对比
| 特性 | Hook(函数组件) | Class 组件 |
|---|---|---|
| 状态管理 | 用 useState 创建局部状态,支持多个独立状态 |
用 this.state 管理状态,需手动绑定 this |
| 副作用处理 | 用 useEffect 聚合相关逻辑 |
分散在 componentDidMount/Update/WillUnmount |
| 逻辑复用 | 自定义 Hook 直接复用,无嵌套 | 依赖 HOC/Render Props,易产生“嵌套地狱” |
| 代码体积 | 代码简洁,无 class 模板代码 | 需编写 constructor、render 等模板代码 |
| 学习成本 | 只需理解函数和 Hook 规则 | 需理解 this 绑定、生命周期、继承等 |
| 调试体验 | 状态与函数作用域绑定,调试直观 | 需追踪 this.state 变化,调试较复杂 |
六、总结
React Hook 是函数组件的核心增强特性,通过 useState、useEffect 等内置 Hook 解决了 class 组件的痛点,同时支持自定义 Hook 实现逻辑复用。掌握 Hook 的关键在于:
- 严格遵循 Hook 使用规则(顶层调用、仅在函数组件/自定义 Hook 中调用)。
- 理解各内置 Hook 的适用场景(如
useState管状态、useEffect管副作用)。 - 合理设计自定义 Hook,抽取通用逻辑,提升代码复用性。
- 结合性能优化 Hook(
useMemo/useCallback)和最佳实践,写出高效、可维护的 React 代码。
目前,Hook 已成为 React 开发的主流方案,React 官方也推荐优先使用 Hook 开发新组件。
更多推荐


所有评论(0)