React Hooks — 是允许在函数组件中使用状态、副作用、上下文和其他React功能的函数,无需编写类。Hooks于React 16.8引入,彻底改变了开发方式,取代了HOC和render props等复杂模式。根据React, 2024,有15个内置Hooks覆盖了组件管理的所有场景。
要点
React Hooks — 是React内置的一组函数,允许在不创建类的情况下“连接”到框架的内部机制。在React 16.8之前,函数组件只能接收props并返回JSX — 任何复杂逻辑都需要类组件。
类组件的问题 — 相关逻辑的分离。属于一个实体的代码(例如WebSocket订阅)被迫分布在componentDidMount、componentDidUpdate和componentWillUnmount之间。Hooks解决了这个问题:所有逻辑都分组在一个useEffect中。
第二个问题 — 逻辑复用。为了在类组件之间复用状态逻辑,必须应用HOC(高阶组件)或render props — 这两种模式都在组件树中创建了额外的嵌套。自定义Hooks在无需嵌套的情况下解决了这个问题。
useState — 最基本的Hook。它接受一个初始值并返回一个元组:当前值和用于更新它的函数。与类中的this.setState不同,useState不会合并对象 — 新状态完全取代特定useState调用的先前状态。
import React, { useState } from 'react';
function Form() {
const [text, setText] = useState('');
const [submitting, setSubmitting] = useState(false);
async function handleSubmit() {
setSubmitting(true);
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ text })
});
setSubmitting(false);
}
return (
<div>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={handleSubmit}
disabled={submitting}>
{submitting ? 'Saving...' : 'Submit'}
</button>
</div>
);
}
useState的设置器可以接受一个函数,该函数接收先前的值并返回新值。当新状态从先前状态计算而来且可能存在批量setState调用时,函数形式是必需的 — 每次调用都将获得最新的先前值。
useEffect — 用于在函数组件中执行副作用的Hook。它接受两个参数:副作用函数和依赖数组。React在每次渲染后执行副作用,但指定依赖项时 — 仅当其中任何一个发生变化时。
空依赖数组[]意味着副作用在首次渲染后执行一次(类似componentDidMount)。从副作用返回函数是清理(cleanup),它在卸载时(类似componentWillUnmount)和副作用重新执行之前执行。
import React, { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const socket = new WebSocket(`wss://chat/${roomId}`);
socket.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
// 清理 - 关闭套接字
return () => socket.close();
}, [roomId]);
return <ul>
{messages.map((msg, i) =>
<li key={i}>{msg.text}</li>
)}
</ul>;
}
useEffect的依赖项 — 是副作用与外部世界之间的唯一连接。如果在副作用中使用了来自props或状态的变量,它必须位于依赖数组中。忽略此规则是出现陈旧数据错误(stale closure)的根源。
useContext — 用于读取上下文值的Hook。它接受由React.createContext创建的上下文对象并返回当前值。如果上下文发生变化,组件会自动用新值重新渲染。
在useContext出现之前,函数组件中访问上下文需要包装在Context.Consumer中,这会产生嵌套。useContext完全消除了Consumer — 只需在函数组件的任何位置调用该Hook即可。
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext('亮色');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button
className={`btn btn-${theme}`}
>
{theme === 'dark' ? 'moon' : '☀️'}
</button>;
}
useContext不是Redux或全局状态的替代品。React建议将上下文用于很少变化的值 — 主题、语言、授权。对于频繁更新的数据,每次重新渲染都会重新计算上下文的所有消费者,这可能会降低性能。
useReducer — 对于具有复杂更新逻辑的状态,是useState的替代方案。它接受reducer函数和初始状态,返回当前状态和dispatch函数。Reducer接收当前状态和action,返回新状态 — 一个没有副作用的纯函数。
当多个操作(set、reset、increment、addItem)管理一个状态时,useReducer特别有用。替代多个具有不同设置器的useState — 一个具有清晰操作类型的reducer。这使得逻辑可预测且易于测试。
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'setStep':
return { ...state, step: action.step };
case 'reset':
return { count: 0, step: 1 };
default:
throw new Error('Unknown action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
step: 1
});
return (
<>
<p>Count: {state.count}</p>
<button
onClick={() => dispatch({ type: 'increment' })}>+</button>
<button
onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}
useRef创建一个在渲染之间持续存在的可变对象。更改current不会导致重新渲染。主要用途 — 访问DOM元素和存储可变值(定时器、先前值、库实例)。
useMemo记忆函数的结果。仅当其中一个依赖项更改时才重新计算值。用于昂贵的计算 — 过滤大数组、排序、格式化数据。useCallback — 用于记忆函数的useMemo特例。
import React, { useRef, useMemo, useCallback } from 'react';
function SearchPage({ items, query, onItemSelect }) {
const inputRef = useRef(null);
// 挂载时自动聚焦
useEffect(() => {
inputRef.current?.focus();
}, []);
// 记忆过滤后的结果
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase()
.includes(query.toLowerCase())
);
}, [items, query]);
// 记忆回调
const handleSelect = useCallback((id) => {
onItemSelect(id);
}, [onItemSelect]);
return (
<div>
<input ref={inputRef}
value={query}
onChange={e => setQuery(e.target.value)}
/>
<ul>
{filtered.map(item =>
<li key={item.id}>
<button
onClick={() => handleSelect(item.id)}>
{item.name}
</button>
</li>
)}
</ul>
</div>
);
}
重要规则:useRef、useMemo和useCallback — 优化工具,而非强制构造。不要将所有内容都包装在useMemo中 — 先衡量性能。过早优化会使代码复杂化。React渲染组件很快;只有在计算确实昂贵(O(n²)及以上)时才需要useMemo。
React为Hooks引入了两条严格的规则。第一:只在顶层调用Hooks — 不在条件、循环或嵌套函数内部。第二:只从React函数组件或自定义Hooks中调用Hooks。ESLint插件eslint-plugin-react-hooks自动检查这两条规则。
最常见的错误 — 违反Hooks的调用顺序。React依赖渲染之间Hooks的调用顺序。如果Hook位于条件内部且未在一次渲染中被调用,所有后续Hook都会移位,导致错误。
| 错误 | 后果 | 解决方案 |
|---|---|---|
| 条件中的Hook | Hooks顺序被打乱 | 将条件逻辑移到Hook内部 |
| Stale closure | 副作用使用了旧值 | 将所有依赖项添加到数组 |
| 无限循环 | 无依赖项的useEffect更改状态 | 指定依赖项或使用useReducer |
| 在渲染中更改ref | 函数体中的副作用 | 将变更移动到useEffect中 |
第二个常见错误 — useEffect中的stale closure。如果在副作用中使用了未在依赖项中指定的变量,副作用会在创建闭包时“捕获”其值。解决方案 — 始终将所有使用的变量包含在依赖数组中。如果依赖项过多,请将副作用拆分为多个部分。
常见问题
React依赖渲染之间Hooks的调用顺序。如果在条件中调用了Hook且在一次渲染中未执行,所有后续Hook都会“偏移” — 它们的状态会混乱。ESLint规则hooks/exhaustive-deps会对此发出警告。
useLayoutEffect是同步的,在浏览器绘制更改之前执行。useEffect是异步的 — 在绘制之后执行。仅在需要在用户看到更改之前测量或更改DOM时才使用useLayoutEffect(弹出窗口位置、滚动恢复)。
在useEffect内部使用AbortController。创建一个controller,将signal传递给fetch,在清理函数中调用controller.abort()。对于旧版浏览器,使用cancelled标志即可:return () => { cancelled = true; },在setState之前进行检查。
自定义Hook — 一个以use为前缀的JavaScript函数,使用内置Hooks。当相同的Hooks逻辑在两个或更多组件中重复时创建。示例:useDebounce、useLocalStorage、useMediaQuery。自定义Hooks取代了HOC和render props。
在Strict Mode中,React有意在开发模式下两次挂载和卸载组件,以发现未清理的副作用中的错误。如果清理函数编写正确,双重调用不会导致问题。在生产模式下不会发生这种情况。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。