当希望组件“记住”某些信息,但又不想让这些信息触发新的渲染时,可以使用 ref
。
ref
导入useRef()
import { useRef } from 'react';
调用useRef
const ref = useRef(0);
可以用 ref.current
属性访问该 ref 的当前值,例如:
ref.current = ref.current + 1;
设置 state 会重新渲染组件,更改 ref 不会!当一条信息用于渲染时,将它保存在 state 中。当一条信息仅被事件处理器需要,并且更改它不需要重新渲染时,使用 ref 可能会更高效。
ref | state |
---|---|
useRef(initialValue) 返回 { current: initialValue } | useRef(initialValue) 返回 { current: initialValue } |
更改时不会触发重新渲染 | 更改时触发重新渲染。 |
可变 —— 可以在渲染过程之外修改和更新 current 的值 | “不可变” —— 必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。 |
不应在渲染期间读取(或写入) current 值。 | 可以随时读取 state 。但是,每次渲染都有自己不变的 state 快照。 |
timeout Id
DOM
元素JSX
的其他对象有时可能需要访问由 React 管理的 DOM 元素 —— 例如,让一个组件获得焦点、滚动到它或测量它的尺寸和位置。在 React 中没有内置的方法来做这些事情,所以需要一个指向 DOM 节点的 ref 来实现。
// 第一步,引入Ref
import { useRef } from 'react';
// 第二步,声明一个ref
const myRef = useRef(null);
// 第三步,将ref传入html标签内,比如
<div ref={myRef}>
如果需要为列表中的每一项都绑定 ref
,而又不知道会有多少项。那么可以将函数传递给ref
属性,称为ref
回调。
import { useRef } from 'react';
export default function CatFriends() {
const itemsRef = useRef(null);
function scrollToId(itemId) {
const map = getMap();
const node = map.get(itemId);
node.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
function getMap() {
if (!itemsRef.current) {
// 首次运行时初始化 Map。
itemsRef.current = new Map();
}
return itemsRef.current;
}
return (
<>
<nav>
<button onClick={() => scrollToId(0)}>
Tom
</button>
<button onClick={() => scrollToId(5)}>
Maru
</button>
<button onClick={() => scrollToId(9)}>
Jellylorum
</button>
</nav>
<div>
<ul>
{catList.map(cat => (
<li
key={cat.id}
ref={(node) => {
const map = getMap();
if (node) {
// 添加到 Map
map.set(cat.id, node);
} else {
// 从 Map 删除
map.delete(cat.id);
}
}}
>
<img
src={cat.imageUrl}
alt={'Cat #' + cat.id}
/>
</li>
))}
</ul>
</div>
</>
);
}
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push({
id: i,
imageUrl: 'https://placekitten.com/250/200?image=' + i
});
}
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
告诉 React 将对应的 DOM 节点放入 inputRef.current
中。但是,这取决于 MyInput
组件是否允许这种行为, 默认情况下是不允许的。MyInput
组件是使用 forwardRef
声明的。 这让从上面接收的 inputRef
作为第二个参数 ref
传入组件,第一个参数是 props
。MyInput
组件将自己接收到的 ref
传递给它内部的
。限制暴露的功能:useImperativeHandle
。
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 只暴露 focus,没有别的
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
如果需要强制 React 同步更新(“刷新”)DOM。从 react-dom
导入 flushSync
并将 state 更新包裹 到 flushSync
调用中:
import { flushSync } from 'react-dom';
function handleAdd() {
const newTodo = { id: nextId++, text: text };
flushSync(() => {
setText('');
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
Refs 是一个应急方案。应该只在必须“跳出 React”时使用它们。这方面的常见示例包括管理焦点、滚动位置或调用 React 未暴露的浏览器 API。
Effects
会在渲染后运行一些代码,以便可以将组件与 React 之外的某些系统同步。不要随意在你的组件中使用 Effect。
三个步骤:
声明Effect
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// 每次渲染后都会执行此处的代码
});
return ;
}
指定Effect
依赖。大多数 Effect 应该按需执行,而不是在每次渲染后都执行。
useEffect(() => {
if (isPlaying) { // isPlaying 在此处使用……
// ...
} else {
// ...
}
}, [isPlaying]); // ……所以它必须在此处声明!
必要时添加清理(cleanUp
)函数。有时 Effect 需要指定如何停止、撤销,或者清除它的效果。
空的依赖数组([]
)对应于组件“挂载”,即添加到屏幕上。
useEffect(() => {
const connection = createConnection(); // 开启连接
connection.connect();
return () => {
connection.disconnect(); // 断开连接
};
}, []);
控制非React组件
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);
订阅事件
如果 Effect 订阅了某些事件,清理函数应该退订这些事件:
useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
触发动画
如果 Effect 对某些内容加入了动画,清理函数应将动画重置
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // 触发动画
return () => {
node.style.opacity = 0; // 重置为初始值
};
}, []);
获取数据
如果 Effect 将会获取数据,清理函数应该要么 中止该数据获取操作,要么忽略其结果
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
为了防止条件竞争,每个 Effect 都可以在里面设置一个 ignore
标记变量。在最开始,ignore
被设置为 false
。然而,当 Effect 执行清理函数后(就像你选中了列表中不同的人时),ignore
就会被设置为 true
。
- 仅在严格模式下的开发环境中,React 会挂载两次组件,以对 Effect 进行压力测试。
- React 将在下次 Effect 运行之前以及卸载期间这两个时候调用清理函数。
根据props
或state
来更新state
使用useMemo
缓存耗时的计算
比如:
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ 除非 todos 或 filter 发生变化,否则不会重新执行 getFilteredTodos()
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}
会告诉 React,除非 todos
或 filter
发生变化,否则不要重新执行传入的函数。
当 props
变化时重置所有 state
。可以使用key
属性来标识。
当prop变化时调整部分state
// 虽然下面这种方式比 Effect 更高效,但大多数组件也不需要它
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 好一些:在渲染期间调整 state
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
/* -----------优化:在渲染期间计算内容---------- */
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ 非常好:在渲染期间计算所需内容
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}
检查是否可以通过添加 key 来重置所有 state,或者 在渲染期间计算所需内容。
在事件处理函数中共享逻辑
如果有组件用到了共同的函数调用,尝试把这个函数抽离出来成为一个独立函数
function ProductPage({ product, addToCart }) {
// ✅ 非常好:事件特定的逻辑在事件处理函数中处理
function buyProduct() {
addToCart(product);
showNotification(`已添加 ${product.name} 进购物车!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}
发送Post
请求
当用户按下按钮发送post
请求,只在特定交互中发生
链式计算
初始化应用
每次应用加载时执行一次。可以添加一个顶层变量来记录它是否已经被执行过了。
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ 只在每次应用加载时执行一次
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}
或者在模块初始化和应用渲染之前执行:
if (typeof window !== 'undefined') { // 检测我们是否在浏览器环境
// ✅ 只在每次应用加载时执行一次
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}
为了避免在导入任意组件时降低性能或产生意外行为,请不要过度使用这种方法。将应用级别的初始化逻辑保留在像 App.js
这样的根组件模块或你的应用入口中。
通知父组件有关state
变化的信息
可以试试状态提升,由父组件控制state
将数据传递给父组件
可以让父组件获取数据,并传递给子组件
订阅外部store
利用react
的Hook函数useSyncExternalStore
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
// ✅ 非常好:用内置的 Hook 订阅外部 store
return useSyncExternalStore(
subscribe, // 只要传递的是同一个函数,React 不会重新订阅
() => navigator.onLine, // 如何在客户端获取值
() => true // 如何在服务端获取值
);
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
获取数据
为了避免条件竞争情况的出现,需要在effect中添加清理函数来忽略较早的返回结果.
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
比使用effect更好的办法是使用框架的内置数据获取机制
每个react组件都经历相同的生命周期
但是effect不一样,Effect 能够在需要时始终具备启动和停止的弹性。
React会通过在开发环境中立即强制 Effect 重新进行同步来验证其是否能够重新同步。而之所以知道需要重新同步,是因为effect
的依赖项发生了变化。
**代码中的每个 Effect 应该代表一个独立的同步过程。**也就是说删除一个 Effect 不会影响另一个 Effect 的逻辑。
Effect
的依赖项是变量,变量发生改变后,effect
会重新响应。
如果effect
没有依赖项,就表明这个effect
仅在组件挂载时执行一次,并在组件卸载时清理。
组件内部的所有值(包括 props、state 和组件体内的变量)都是响应式的。任何响应式值都可以在重新渲染时发生变化,所以需要将响应式值包括在 Effect 的依赖项中。
全局变量或可变值不可以作为依赖。应该使用
useSyncExternalStore
来读取和订阅外部可变值。
如果出现无限循环的问题,或者 Effect 过于频繁地重新进行同步,可以尝试以下解决方案:
Effect
是否表示了独立的同步过程。挑战:一个下拉框允许用户选择一个行星,而另一个下拉框应该显示该选定行星上的地点。然而,目前这两个下拉框都还没有正常工作。你的任务是添加一些额外的代码,使得选择一个行星时,placeList
状态变量被填充为 "/planets/" + planetId + "/places"
API 调用的结果。
App.js
import { useState, useEffect } from 'react';
import { fetchData } from './api.js';
export default function Page() {
const [planetList, setPlanetList] = useState([])
const [planetId, setPlanetId] = useState('');
const [placeList, setPlaceList] = useState([]);
const [placeId, setPlaceId] = useState('');
useEffect(() => {
let ignore = false;
fetchData('/planets').then(result => {
if (!ignore) {
console.log('获取了一个行星列表。');
setPlanetList(result);
setPlanetId(result[0].id); // 选择第一个行星
}
});
return () => {
ignore = true;
}
}, []);
useEffect(() => {
if (planetId === '') {
return;
}
let ignore = false;
fetchData('/planets/' + planetId + '/places').then(result => {
if (!ignore) {
console.log('获取了该行星的地点列表');
setPlaceList(result);
setPlaceId(result[0].id);
}
});
return () => {
ignore = true;
}
}, [planetId])
return (
<>
<label>
选择一个行星:{' '}
<select value={planetId} onChange={e => {
setPlanetId(e.target.value);
}}>
{planetList?.map(planet =>
<option key={planet.id} value={planet.id}>{planet.name}</option>
)}
</select>
</label>
<label>
选择一个地点:{' '}
<select value={placeId} onChange={e => {
setPlaceId(e.target.value);
}}>
{placeList?.map(place =>
<option key={place.id} value={place.id}>{place.name}</option>
)}
</select>
</label>
<hr />
<p>你将要前往:{planetId || '...'} 的 {placeId || '...'} </p>
</>
);
}
理想情况下,应用程序中的大多数 Effect
最终都应该由自定义 Hook
替代,无论是由你自己编写还是由社区提供。为了减少一些重复,可以把一些逻辑提取到自定义Hook
中。
App.js
import { useState } from 'react';
import { useSelectOptions } from './useSelectOptions.js';
export default function Page() {
const [
planetList,
planetId,
setPlanetId
] = useSelectOptions('/planets');
const [
placeList,
placeId,
setPlaceId
] = useSelectOptions(planetId ? `/planets/${planetId}/places` : null);
return (
<>
<label>
选择一个行星:{' '}
<select value={planetId} onChange={e => {
setPlanetId(e.target.value);
}}>
{planetList?.map(planet =>
<option key={planet.id} value={planet.id}>{planet.name}</option>
)}
</select>
</label>
<label>
选择一个地点:{' '}
<select value={placeId} onChange={e => {
setPlaceId(e.target.value);
}}>
{placeList?.map(place =>
<option key={place.id} value={place.id}>{place.name}</option>
)}
</select>
</label>
<hr />
<p>你将要前往:{planetId || '...'} 的 {placeId || '...'} </p>
</>
);
}
useSelectOptions.js
import { useState, useEffect } from 'react';
import { fetchData } from './api.js';
export function useSelectOptions(url) {
const [list, setList] = useState(null);
const [selectedId, setSelectedId] = useState('');
useEffect(() => {
if (url === null) {
return;
}
let ignore = false;
fetchData(url).then(result => {
if (!ignore) {
setList(result);
setSelectedId(result[0].id);
}
});
return () => {
ignore = true;
}
}, [url]);
return [list, selectedId, setSelectedId];
}
事件处理函数:
Effect:
组件内部声明的 state 和 props 变量被称为响应式值。这些响应式值参与组件的渲染数据流。
声明一个Effect Event
:尚未发布到React正式版中(截至2023.8.15)。
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
之后可以在Effect
内部调用onConnected
:
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 声明所有依赖项
尚未发布到React正式版中(截至2023.8.15)。
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ 声明所有依赖项
// ...
}
这里的 onVisit
是一个 Effect Event。里面的代码不是响应式的。另一方面,Effect 本身仍然是响应式的。其内部的代码使用了 url
props,所以每次因为不同的 url
重新渲染后 Effect 都会重新运行。这会依次调用 onVisit
这个 Effect Event。
Effect Event 的局限性在于你如何使用他们:
**建议将依赖性 lint 错误作为一个编译错误来处理。**不然有可能会遇到你并不知道是什么的bug。
需要考虑的问题:
这段代码应该移到事件处理程序中吗?
避免Effect
中有特定的事件处理逻辑代码。
Effect是否在做几件不相关的事情?
每个effect
应该代表一个独立的同步过程。如果担心代码重复,可以提取相同逻辑到自定义Hook来提升代码质量。
是否在读取一些状态来计算下一个状态?
将非响应式逻辑移至Effect Event
中(正式版未发布)
用 Effect Event
包装来自props
的事件处理程序
尽可能避免将对象和函数作为 Effect 的依赖
将静态对象和函数移除组件
将动态对象和函数移动到effect中
从对象中读取原始值
从 Effect 外部 读取对象信息,并避免依赖对象和函数类型:
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
const { roomId, serverUrl } = options;
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ 所有依赖已声明
// ...
从函数中计算原始值
假设父组件传了一个函数:
<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>
为避免使其成为依赖(并导致它在重新渲染时重新连接),需要在 Effect 外部调用它:
function ChatRoom({ getOptions }) {
const [message, setMessage] = useState('');
const { roomId, serverUrl } = getOptions();
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ 所有依赖已声明
// ...
这仅适用于 纯函数,因为它们在渲染期间可以安全调用。如果函数是一个事件处理程序,但你不希望它的更改重新同步 Effect,将它包装到 Effect Event 中。
如果一个Effect中的逻辑有多个组件用到了,就可以考虑将重复逻辑部分提取出来。
Hook的名称必须以’use’开头!
自定义Hook共享的是状态逻辑,而不是状态本身。对同一个 Hook 的每个调用是各自完全独立的。
使用了useEffect
import { useEffect, useEffectEvent } from 'react';
// ...
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ 声明所有依赖
}
首先明白一件事,就是如果你需要写Effect就意味着需要"走出React"和某些外部系统同步,或者需要做一些react中没有对应内置API的事。
使用自定义Hook时需要专注于高级用例,避免使用react生命周期,比如useMount
,每个自定义Hook应该专注于实现一个功能。
把Effect包裹进自定义Hook有益的另一些原因: