React
快速入门
React 应用程序是由 组件 组成的。一个组件是 UI(用户界面)的一部分,它拥有自己的逻辑和外观。组件可以小到一个按钮,也可以大到整个页面。
React 组件是返回标签的 JavaScript 函数:
function MyButton() {
return <button>我是一个按钮</button>;
}
注
React 组件必须以大写字母开头,而 HTML 标签则必须是小写字母。
使用 JSX 编写标签
JSX 比 HTML 更加严格。你必须闭合标签,如 <br />
。你的组件也不能返回多个 JSX 标签。你必须将它们包裹到一个共享的父级中,比如 <div>...</div>
或使用空的 <></>
包裹:
添加样式
在 React 中,你可以使用 className 来指定一个 CSS 的 class。它与 HTML 的 class 属性的工作方式相同
<img className="avatar" />
显示数据
JSX 会让你把标签放到 JavaScript 中。而大括号会让你 “回到” JavaScript 中,这样你就可以从你的代码中嵌入一些变量并展示给用户。例如,这将显示 user.name
:
return <h1>{user.name}</h1>;
你还可以将 JSX 属性 “转义到 JavaScript”,但你必须使用大括号 而非 引号。例如,className="avatar"
是将 "avatar" 字符串传递给 className,作为 CSS 的 class。但 src={user.imageUrl}
会读取 JavaScript 的 user.imageUrl
变量,然后将该值作为 src 属性传递:
return <img className="avatar" src={user.imageUrl} />;
条件渲染
React 没有特殊的语法来编写条件语句,因此你使用的就是普通的 JavaScript 代码。
let content;
if (isLoggedIn) {
content = <AdminPanel />;
} else {
content = <LoginForm />;
}
return <div>{content}</div>;
也可以使用三元运算符
<div>{isLoggedIn ? <AdminPanel /> : <LoginForm />}</div>
<div>
{isLoggedIn && <AdminPanel />}
</div>
列表渲染
你将依赖 JavaScript 的特性,例如 for 循环 和 array 的 map() 函数 来渲染组件列表。
const products = [
{ title: "Cabbage", id: 1 },
{ title: "Garlic", id: 2 },
{ title: "Apple", id: 3 },
];
const listItems = products.map((product) => <li key={product.id}>{product.title}</li>);
return <ul>{listItems}</ul>;
响应事件
你可以通过在组件中声明 事件处理 函数来响应事件:
function MyButton() {
function handleClick() {
alert("You clicked me!");
}
return <button onClick={handleClick}>点我</button>;
}
// 传参数的写法
function MyButton() {
function handleClick(name) {
alert("Hello, " + name);
}
return <button onClick={() => handleClick("Evan")}>点我</button>;
}
注意,onClick={handleClick}
的结尾没有小括号!不要 调用 事件处理函数:你只需 把函数传递给事件 即可。当用户点击按钮时 React 会调用你传递的事件处理函数。
使用 Hook
以 use
开头的函数被称为 Hook。useState
是 React 提供的一个内置 Hook。你可以在 React API 参考 中找到其他内置的 Hook。你也可以通过组合现有的 Hook 来编写属于你自己的 Hook。
Hook 比普通函数更为严格。你只能在你的组件(或其他 Hook)的 顶层 调用 Hook。如果你想在一个条件或循环中使用 useState
,请提取一个新的组件并在组件内部使用它。
Props
可以通过 props
将数据传递给组件。
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<MyButton count={count} onClick={handleClick} />
</div>
);
}
function MyButton({ count, onClick }) {
return <button onClick={onClick}>点了 {count} 次</button>;
}
React Hooks
Hook 可以帮助在组件中使用不同的 React 功能。你可以使用内置的 Hook 或使用自定义 Hook。
useState
在组件的顶层调用 useState
来声明一个状态变量。
const [state, setState] = useState(initialState);
参数
initialState
:初始状态,可以是任何类型的值,也可以是纯函数(直接传递初始化函数本身,而不是函数的调用结果)
返回值
state
:当前状态setState
:更新状态的函数,它可以让你将 state 更新为不同的值并触发重新渲染
注意
useState
返回的 setState
函数允许你将 state 更新为不同的值并触发重新渲染。你可以直接传递新状态,也可以传递一个根据先前状态来计算新状态的函数:
注
调用 setState
函数 不会 改变已经执行的代码中当前的 state。
它只影响 下一次 渲染中 useState
返回的内容。
const [name, setName] = useState("Taylor");
function handleClick() {
setName("Robin");
console.log(name); // Still "Taylor"!
}
1️⃣ 直接传递新状态
const [age, setAge] = useState(42);
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
点击一次后,age
将只会变为 43 而不是 45!这是因为调用 set
函数 不会更新 已经运行代码中的 age
状态变量。因此,每个 setAge(age + 1)
调用变成了 setAge(43)
。
为了解决这个问题,你可以向 setAge
传递一个 更新函数,而不是下一个状态:
2️⃣ 传递更新函数
const [age, setAge] = useState(42);
function handleClick() {
setAge((a) => a + 1); // setAge(42 => 43)
setAge((a) => a + 1); // setAge(43 => 44)
setAge((a) => a + 1); // setAge(44 => 45)
}
这里,a => a + 1
是更新函数。它获取 待定状态 并从中计算 下一个状态。
// ✅ 使用新对象替换 state
setForm({
...form,
firstName: "Taylor",
});
使用 key 重置状态
你可以通过向组件传递不同的 key
来重置组件的状态。在这个例子中,重置按钮改变 version
状态变量,我们将它作为一个 key
传递给 Form
组件。当 key
改变时,React 会从头开始重新创建 Form
组件(以及它的所有子组件),所以它的状态被重置了。
export default function App() {
const [version, setVersion] = useState(0);
function handleReset() {
setVersion(version + 1);
}
return (
<>
<button onClick={handleReset}>Reset</button>
<Form key={version} />
</>
);
}
始终在状态中替换对象和数组
如果下一个状态等于先前的状态,React 将忽略你的更新,这是由 Object.is 比较确定的。这通常发生在你直接更改状态中的对象或数组时:
obj.x = 10; // 🚩 错误:直接修改现有的对象
setObj(obj); // 🚩 不会发生任何事情
你修改了一个现有的 obj
对象并将其传递回 setObj
,因此 React 忽略了更新。为了解决这个问题,你需要确保始终在状态中 替换 对象和数组,而不是对它们进行 更改:
// ✅ 正确:创建一个新对象
setObj({
...obj,
x: 10,
});
将 state 初始化为函数
如果需要用 state
变量来存储函数,不能直接把函数赋值给 state
,而是需要用箭头函数来赋值。
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}
useReducer
在组件的顶层调用 useReducer
来管理复杂的 state
逻辑。简单来说,useReducer
可以让你像写 Redux 一样管理本地 state
。
const [state, dispatch] = useReducer(reducer, initialArg, init?)
参数
reducer
:一个函数,它接收当前的state
和要执行的action
,并返回下一个state
。initialArg
:用于初始化state
的任意值。初始值的计算逻辑取决于接下来的init
参数init
:用于计算初始值的函数。如果存在,使用init(initialArg)
的执行结果作为初始值,否则使用initialArg
返回值
state
:当前状态。初次渲染时,它是init(initialArg)
或initialArg
(如果没有init
函数)。dispatch
:一个函数,它接收一个action
并调用reducer
来计算下一个state
。
通常来说 action
是一个对象,其中 type
属性标识类型,其它属性携带额外信息。
示例
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
}
// init 函数:接收 initialArg,返回初始 state
function init(initialCount) {
return { count: initialCount * 2 };
}
export default function Counter({ initialCount }) {
// 这里用到了 initialArg 和 init
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
// 使用
<Counter initialCount={5} />;
注意
dispatch
函数 是为下一次渲染而更新 state
。因此在调用 dispatch
函数后读取 state
并不会拿到更新后的值,也就是说只能获取到调用前的值。
useReducer
和 useState
非常相似,但是它可以让你把状态更新逻辑从事件处理函数中移动到组件外部。详情可以参阅 对比 useState 和 useReducer。
useContext
上下文帮助组件 从祖先组件接收信息,而无需将其作为 props
传递。例如,应用程序的顶层组件可以借助上下文将 UI 主题传递给所有下方的组件,无论这些组件层级有多深。
const value = useContext(SomeContext);
示例
const ThemeContext = createContext(null); // 创建上下文,默认值为 null
export default function MyPage() {
return (
<ThemeContext value="dark">
<MyButton />
</ThemeContext>
);
}
function MyButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Submit</button>;
}
useRef
ref
允许组件 保存一些不用于渲染的信息。更新 ref
不会重新渲染组件。
ref
是从 React 范例中的“脱围机制”。当需要与非 React 系统如浏览器内置 API 一同工作时,ref
将会非常有用。
const ref = useRef(initialValue);
参数
initialValue
:ref 对象的 current 属性的初始值。可以是任意类型的值。这个参数在首次渲染后被忽略。
返回值
ref
:一个包含current
属性的对象。current
属性初始值为传递的initialValue
。之后可以将其设置为其他值。如果将ref
对象作为一个 JSX 节点的ref
属性传递给 React,React 将为它设置current
属性。
注意
改变 ref
不会触发重新渲染。这意味着 ref
是存储一些不影响组件视图输出信息的完美选择。
不要在渲染期间写入或者读取 ref.current
。
示例
存储定时器 ID
const intervalRef = useRef(null);
// 开启定时器
function handleStartClick() {
const intervalId = setInterval(() => {
// ...
}, 1000);
intervalRef.current = intervalId;
}
// 清除定时器
function handleStopClick() {
const intervalId = intervalRef.current;
clearInterval(intervalId);
}
ref 操作 DOM
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>聚焦输入框</button>
</>
);
}
避免重复创建 ref
function Video() {
const playerRef = useRef(null);
if (playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
// ...
}
获取自定义组件的 ref
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
export default function MyInput({ value, onChange, ref }) {
return <input value={value} onChange={onChange} ref={ref} />;
}
获取自定义组件实例
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
export default function MyInput({ value, onChange, ref }) {
const [instance, setInstance] = useState(componentInstance);
useImperativeHandle(ref, () => instance, [instance]); // 自定义由 ref 暴露出来的句柄
return <input value={value} onChange={onChange} ref={ref} />;
}
useImperativeHandle
它能让你自定义由 ref
暴露出来的句柄。
useImperativeHandle(ref, createHandle, dependencies?)
示例
仅暴露 focus
和 scrollIntoView
方法,而不是暴露整个 inputRef
对象。
export default function MyInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(
ref,
() => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
},
[]
);
return <input ref={inputRef} />;
}
useEffect
Effect 允许组件 连接到外部系统并与之同步。这包括处理网络、浏览器、DOM、动画、使用不同 UI 库编写的小部件以及其他非 React 代码。
useEffect(setup, dependencies?)
示例
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState("https://localhost:1234");
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
return <h1>Welcome to the {roomId} room!</h1>;
}
参数
setup
:处理 Effect 的函数。setup 函数选择性返回一个 清理(cleanup) 函数。当组件被添加到 DOM 的时候,React 将运行 setup 函数。在每次依赖项变更重新渲染后,React 将首先使用旧值运行 cleanup 函数(如果你提供了该函数),然后使用新值运行 setup 函数。在组件从 DOM 中移除后,React 将最后一次运行 cleanup 函数。dependencies
:setup
代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。如果只需要运行一次 Effect 函数,则可以传入空数组[]
。
返回值
返回 undefined
。
注意
- 如果你 没有打算与某个外部系统同步,那么你可能不需要 Effect。
- Effect 只在客户端上运行,在服务端渲染中不会运行。
- 你的
cleanup
逻辑应该与setup
逻辑“对称”,并且应该停止或撤销任何setup
做的事情。
案例
1️⃣ 连接服务器
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]);
2️⃣ 监听浏览器事件
useEffect(() => {
function handleMove(e) {
setPosition({ x: e.clientX, y: e.clientY });
}
window.addEventListener("pointermove", handleMove);
return () => {
window.removeEventListener("pointermove", handleMove);
};
}, []);
3️⃣ 触发动画效果
useEffect(() => {
const animation = new FadeInAnimation(ref.current);
animation.start(1000);
return () => {
animation.stop();
};
}, []);
4️⃣ 控制模态框
useEffect(() => {
if (!isOpen) {
return;
}
const dialog = ref.current;
dialog.showModal();
return () => {
dialog.close();
};
}, [isOpen]);
5️⃣ 跟踪元素可见性
useEffect(() => {
const div = ref.current;
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
document.body.style.backgroundColor = "black";
document.body.style.color = "white";
} else {
document.body.style.backgroundColor = "white";
document.body.style.color = "black";
}
},
{
threshold: 1.0,
}
);
observer.observe(div);
return () => {
observer.disconnect();
};
}, []);
在自定义 Hook 中封装 Effect
Effect 是一种 脱围机制:当你需要“走出 React 之外”或者当你的使用场景没有更好的内置解决方案时,你可以使用它们。如果你发现自己经常需要手动编写 Effect,那么这通常表明你需要为组件所依赖的通用行为提取一些 自定义 Hook。
例如,这个 useChatRoom 自定义 Hook 把 Effect 的逻辑“隐藏”在一个更具声明性的 API 之后:
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId,
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
// 你可以像这样从任何组件使用它
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState("https://localhost:1234");
useChatRoom({ roomId, serverUrl });
// ...
}
// 监听窗口事件
export function useWindowListener(eventType, listener) {
useEffect(() => {
window.addEventListener(eventType, listener);
return () => {
window.removeEventListener(eventType, listener);
};
}, [eventType, listener]);
}
// 跟踪元素可见性
export function useIntersectionObserver(ref) {
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const div = ref.current;
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
setIsIntersecting(entry.isIntersecting);
},
{
threshold: 1.0,
}
);
observer.observe(div);
return () => {
observer.disconnect();
};
}, [ref]);
return isIntersecting;
}
避免竞争条件的影响
注意,ignore
变量被初始化为 false
,并且在 cleanup
中被设置为 true
。这样可以确保 你的代码不会受到“竞争条件”的影响:网络响应可能会以与你发送的不同的顺序到达。
export default function Page() {
const [person, setPerson] = useState("Alice");
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then((result) => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);
return <>...</>;
}
指定响应式依赖项
你无法“选择” Effect 的依赖项。Effect 代码中使用的每个 响应式值 都必须声明为依赖项。你的 Effect 依赖列表是由周围代码决定的:
删除不必要的对象依赖项
如果你的 Effect 依赖于在渲染期间创建的对象或函数,则它可能会频繁运行。例如,此 Effect 在每次渲染后都重新连接,因为 options 对象每次渲染都不同
function ChatRoom({ roomId }) {
const options = {
// 🚩 这个对象在每次渲染时都是从头创建的
serverUrl: serverUrl,
roomId: roomId,
};
useEffect(() => {
const connection = createConnection(options); // 它在 Effect 内部使用
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 因此,这些依赖在重新渲染时总是不同的
// ...
}
避免使用渲染期间创建的对象作为依赖项。相反,在 Effect 内部创建对象
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId,
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
或者使用 useMemo
来缓存对象
const options = useMemo(() => {
return {
serverUrl: serverUrl,
roomId: roomId,
};
}, [roomId]);
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ 只重新连接一次
useLayoutEffect
useLayoutEffect
与 useEffect
非常相似,但它会在浏览器重新绘制屏幕之前触发。这意味着你可以读取 DOM 并测量它。如果你需要做与 DOM 相关的操作,请使用 useLayoutEffect
而不是 useEffect
。
useLayoutEffect(setup, dependencies?)
示例
想象一下悬停时出现在某个元素旁边的 tooltip。如果有足够的空间,tooltip 应该出现在元素的上方,但是如果不合适,它应该出现在下面。为了让 tooltip 渲染在最终正确的位置,你需要知道它的高度(即它是否适合放在顶部)。
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0); // 你还不知道真正的高度
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // 现在重新渲染,你知道了真实的高度
}, []);
// ... 在下方的渲染逻辑中使用 tooltipHeight ...
}
下面是这如何一步步工作的
- Tooltip 使用初始值
tooltipHeight = 0
进行渲染(因此 tooltip 可能被错误地放置)。 - React 将它放在 DOM 中,然后运行
useLayoutEffect
中的代码。 useLayoutEffect
测量 了 tooltip 内容的高度,并立即触发重新渲染。- 使用实际的
tooltipHeight
再次渲染 Tooltip(这样 tooltip 的位置就正确了)。 - React 在 DOM 中对它进行更新,浏览器最终显示出 tooltip。
注
注意,即使 Tooltip 组件需要两次渲染(首先,使用初始值为 0 的 tooltipHeight
渲染,然后使用实际测量的高度渲染),你也只能看到最终结果。
React 保证了 useLayoutEffect
中的代码以及其中任何计划的状态更新都会在浏览器重新绘制屏幕之前得到处理。这样你就可以渲染 tooltip,测量它,然后在用户没有注意到第一个额外渲染的情况下再次重新渲染。换句话说,useLayoutEffect
阻塞了浏览器的绘制。
useInsertionEffect
在 React 对 DOM 进行更改之前触发,库可以在此处插入动态 CSS
useInsertionEffect
是为 CSS-in-JS 库的作者特意打造的。除非你正在使用 CSS-in-JS 库并且需要注入样式,否则你应该使用 useEffect
或者 useLayoutEffect
。
useMemo
使用 useMemo
缓存计算代价昂贵的计算结果
const cachedValue = useMemo(calculateValue, dependencies);
参数
calculateValue
:计算值的函数。dependencies
:计算值的依赖项。
返回值
返回计算值的缓存结果。
示例
export default function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}
跳过组件的重新渲染
默认情况下,当一个组件重新渲染时,React 会递归地重新渲染它的所有子组件。使用 memo
可以跳过子组件的重新渲染。
当它的 props
跟上一次渲染相同的时候它就会跳过本次渲染
import { memo } from "react";
const List = memo(function List({ items }) {
// ...
});
记忆一个函数
假设 Form 组件被包裹在 memo
中,你想将一个函数作为 props 传递给它:
export default function ProductPage({ productId, referrer }) {
function handleSubmit(orderDetails) {
post("/product/" + productId + "/buy", {
referrer,
orderDetails,
});
}
return <Form onSubmit={handleSubmit} />;
}
正如 {}
每次都会创建不同的对象一样,像 function() {}
这样的函数声明和像 () => {}
这样的表达式在每次重新渲染时都会产生一个 不同 的函数。就其本身而言,创建一个新函数不是问题。这不是可以避免的事情!但是,如果 Form 组件被记忆了,大概你想在没有 props 改变时跳过它的重新渲染。总是 不同的 props 会破坏你的记忆化。
要使用 useMemo 记忆函数,你的计算函数必须返回另一个函数:
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post("/product/" + productId + "/buy", {
referrer,
orderDetails,
});
};
}, [productId, referrer]);
这太笨拙了,可以使用 useCallback
来记忆函数
const handleSubmit = useCallback(
(orderDetails) => {
post("/product/" + productId + "/buy", {
referrer,
orderDetails,
});
},
[productId, referrer]
);
useCallback
允许你在多次渲染中缓存函数
const cachedFn = useCallback(fn, dependencies);
示例
const handleSubmit = useCallback(
(orderDetails) => {
post("/product/" + productId + "/buy", {
referrer,
orderDetails,
});
},
[productId, referrer]
);
从记忆化回调中更新 state
我们期望记忆化函数具有尽可能少的依赖,当你读取 state 只是为了计算下一个 state 时,你可以通过传递 updater function 以移除该依赖:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos((todos) => [...todos, newTodo]);
}, []); // ✅ 不需要 todos 依赖项
// ...
}
在这里,并不是将 todos
作为依赖项并在内部读取它,而是传递一个关于 如何 更新 state 的指示器 (todos => [...todos, newTodo])
给 React。
防止频繁触发 Effect
有时,你想要在 Effect 内部调用函数:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState("");
function createOptions() {
return {
serverUrl: "https://localhost:1234",
roomId: roomId,
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
// ...
});
}
这会产生一个问题,每一个响应值都必须声明为 Effect 的依赖。但是如果将 createOptions
声明为依赖,它会导致 Effect 不断重新连接到聊天室
(createOptions
函数在每次重新渲染时都会重新创建)
为了解决这个问题,需要在 Effect 中将要调用的函数包裹在 useCallback
中
function ChatRoom({ roomId }) {
const [message, setMessage] = useState("");
const createOptions = useCallback(() => {
return {
serverUrl: "https://localhost:1234",
roomId: roomId,
};
}, [roomId]); // ✅ 仅当 roomId 更改时更改
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ 仅当 createOptions 更改时更改
// ...
}
但是,最好消除对函数依赖项的需求。
function ChatRoom({ roomId }) {
const [message, setMessage] = useState("");
useEffect(() => {
// ✅ 无需使用回调或函数依赖!
function createOptions() {
return {
serverUrl: "https://localhost:1234",
roomId: roomId,
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 仅当 roomId 更改时更改
// ...
}
优化自定义 Hook
如果你正在编写一个 自定义 Hook,建议将它返回的任何函数包裹在 useCallback
中
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback(
(url) => {
dispatch({ type: "navigate", url });
},
[dispatch]
);
const goBack = useCallback(() => {
dispatch({ type: "back" });
}, [dispatch]);
return {
navigate,
goBack,
};
}