4 应急方案
4.1 使用ref引用值
当希望组件“记住”某些信息,但又不想让这些信息触发新的渲染时,可以使用 ref
。
4.1.1 给组件添加ref
导入useRef()
import { useRef } from 'react';
调用useRef
const ref = useRef(0);
可以用 ref.current
属性访问该 ref 的当前值,例如:
ref.current = ref.current + 1;
设置 state 会重新渲染组件,更改 ref 不会!当一条信息用于渲染时,将它保存在 state 中。当一条信息仅被事件处理器需要,并且更改它不需要重新渲染时,使用 ref 可能会更高效。
4.1.2 ref和state的不同之处
ref | state |
---|---|
useRef(initialValue) 返回 { current: initialValue }
|
useRef(initialValue) 返回 { current: initialValue }
|
更改时不会触发重新渲染 | 更改时触发重新渲染。 |
可变 —— 可以在渲染过程之外修改和更新 current 的值 |
“不可变” —— 必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。 |
不应在渲染期间读取(或写入) current 值。 |
可以随时读取 state 。但是,每次渲染都有自己不变的 state 快照。 |
4.1.3 何时使用ref
- 存储
timeout Id
- 存储和操作
DOM
元素 - 存储不需要被用来计算
JSX
的其他对象
4.2 使用ref操作DOM
有时可能需要访问由 React 管理的 DOM 元素 —— 例如,让一个组件获得焦点、滚动到它或测量它的尺寸和位置。在 React 中没有内置的方法来做这些事情,所以需要一个指向 DOM 节点的 ref 来实现。
4.2.1 获取指向节点的ref
// 第一步,引入Ref
import { useRef } from 'react';
// 第二步,声明一个ref
const myRef = useRef(null);
// 第三步,将ref传入html标签内,比如
<div ref={myRef}>
4.2.3 使用 ref 回调管理 ref 列表
如果需要为列表中的每一项都绑定 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
});
}
4.2.4 访问另一个组件的DOM节点
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
-
<MyInput ref={inputRef} />
告诉 React 将对应的 DOM 节点放入inputRef.current
中。但是,这取决于MyInput
组件是否允许这种行为, 默认情况下是不允许的。 -
MyInput
组件是使用forwardRef
声明的。 这让从上面接收的inputRef
作为第二个参数ref
传入组件,第一个参数是props
。 -
MyInput
组件将自己接收到的ref
传递给它内部的<input>
。
限制暴露的功能:useImperativeHandle
。
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 只暴露 focus,没有别的
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
4.2.5 用 flushSync 同步更新 state
如果需要强制 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。
4.3 使用Effect同步
Effects
会在渲染后运行一些代码,以便可以将组件与 React 之外的某些系统同步。不要随意在你的组件中使用 Effect。
三个步骤:
-
声明
Effect
import { useEffect } from 'react'; function MyComponent() { useEffect(() => { // 每次渲染后都会执行此处的代码 }); return <div />; }
-
指定
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 运行之前以及卸载期间这两个时候调用清理函数。
4.4 不需要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更好的办法是使用框架的内置数据获取机制
4.5 响应式Effect的生命周期
4.5.1 effect的生命周期
每个react组件都经历相同的生命周期
- 组件挂载
- 组件更新
- 组件卸载
但是effect不一样,Effect 能够在需要时始终具备启动和停止的弹性。
React会通过在开发环境中立即强制 Effect 重新进行同步来验证其是否能够重新同步。而之所以知道需要重新同步,是因为effect
的依赖项发生了变化。
**代码中的每个 Effect 应该代表一个独立的同步过程。**也就是说删除一个 Effect 不会影响另一个 Effect 的逻辑。
4.5.2 Effect只应该依赖变量
Effect
的依赖项是变量,变量发生改变后,effect
会重新响应。
如果effect
没有依赖项,就表明这个effect
仅在组件挂载时执行一次,并在组件卸载时清理。
组件内部的所有值(包括 props、state 和组件体内的变量)都是响应式的。任何响应式值都可以在重新渲染时发生变化,所以需要将响应式值包括在 Effect 的依赖项中。
全局变量或可变值不可以作为依赖。应该使用
useSyncExternalStore
来读取和订阅外部可变值。
如果出现无限循环的问题,或者 Effect 过于频繁地重新进行同步,可以尝试以下解决方案:
- 检查
Effect
是否表示了独立的同步过程。 - 如果想读取 props 或 state 的最新值,但又不想对其做出反应并重新同步 Effect,可以将 Effect 拆分为具有反应性的部分(保留在 Effect 中)和非反应性的部分(提取为名为 “Effect Event” 的内容)
- 避免将对象和函数作为依赖项
挑战:一个下拉框允许用户选择一个行星,而另一个下拉框应该显示该选定行星上的地点。然而,目前这两个下拉框都还没有正常工作。你的任务是添加一些额外的代码,使得选择一个行星时,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];
}
4.6 将事件从Effect中分开
4.6.1 在事件处理函数和Effect中做选择
事件处理函数:
- 只在响应特定的交互操作时运行
- 事件处理函数内部的逻辑是非响应式的
Effect:
- 需要同步时,比如保持服务器连接
- Effect内部的逻辑是响应式的
组件内部声明的 state 和 props 变量被称为响应式值。这些响应式值参与组件的渲染数据流。
4.6.2 从Effect中提取非响应式逻辑
声明一个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]); // ✅ 声明所有依赖项
4.6.3 使用Effect Event 读取最新的props和state
尚未发布到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 的局限性在于你如何使用他们:
- 只在 Effect 内部调用他们。
- 永远不要把他们传给其他的组件或者 Hook。
4.7 移除Effect依赖
4.7.1 依赖应该和代码保持一致
- 编写 Effect 时,无论这个 Effect 要做什么,首先要明确其生命周期,什么时候同步,什么时候需要清理。
- 确保Effect的依赖项与函数代码里的变量一致。每个被Effect使用的响应值必须在依赖中声明。
- 要移除一个依赖,需要向linter证明其不需要这个依赖。
- 要改变依赖,也要考虑代码需不需要改变。
**建议将依赖性 lint 错误作为一个编译错误来处理。**不然有可能会遇到你并不知道是什么的bug。
4.7.2 移除非必须的依赖
需要考虑的问题:
-
这段代码应该移到事件处理程序中吗?
避免
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 中。
4.8 使用自定义Hook复用逻辑
4.8.1 从组件中提取自定义Hook
如果一个Effect中的逻辑有多个组件用到了,就可以考虑将重复逻辑部分提取出来。
Hook的名称必须以’use’开头!
自定义Hook共享的是状态逻辑,而不是状态本身。对同一个 Hook 的每个调用是各自完全独立的。
4.8.2 把事件处理函数传到自定义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]); // ✅ 声明所有依赖
}
4.8.3 什么时候使用自定义Hook
首先明白一件事,就是如果你需要写Effect就意味着需要"走出React"和某些外部系统同步,或者需要做一些react中没有对应内置API的事。
使用自定义Hook时需要专注于高级用例,避免使用react生命周期,比如useMount
,每个自定义Hook应该专注于实现一个功能。文章来源:https://www.toymoban.com/news/detail-699185.html
把Effect包裹进自定义Hook有益的另一些原因:文章来源地址https://www.toymoban.com/news/detail-699185.html
- 让进出 Effect 的数据流非常清晰。
- 让组件专注于目标,而不是 Effect 的准确实现。
- 当 React 增加新特性时,可以在不修改任何组件的情况下移除这些 Effect。
到了这里,关于【React】React学习:从初级到高级(四)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!