useEffect
- Effect 里是与外部系统(如Brower API、非React 小部件、network)同步,即时反应的。
- 如果useEffect的依赖项列表中含有你不期望会随着它变化而重新渲染的值(非反应式的),可以把涉及到的Event写入useEffectEvent中。
- 依赖项列表是否变化,React使用
Object.js()
与上一个进行比较。所以依赖项不要放object
。
在开发环境,为何useEffect内代码执行两次?
因为React会在初始挂载后,立即重新挂载组件一次,为了验证你是否实现cleanup函数。在生产环境只会加载一次;也可以关闭严格模式,也只会加载一次(但尽量不要关闭)
在Effect内fetch data的缺点:
(1) Effect不会在服务器上运行,意味着初始呈现的HTML不包含data,需要客户端自己发请求;
(2) 直接在Effect中获取,意味着不能预加载或者缓存数据。如果组件重载,会再次获取数据;
建议构建客户端缓存。流行的开源解决方案包括React Query、useSWR、和React Router 6.4+;
也可以构建自己的解决方案,添加以下逻辑:删除重复请求,缓存响应和避免网络瀑布
不需要Effect的情况⭐
通过props or state 的计算得到的变量,不要写在state中,直接在组件顶层计算。
如果数据有很多条,计算复杂。每次重新渲染都要重新计算,影响性能。React有内置的HookuseMemo
缓存数据。
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ Does not re-run unless todos or filter change
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}
如果todos or filter
任一发生变化,会再次执行getFilteredTodos()
如何判断计算是否耗时?
console.time('filter array');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter array');
开发时计算机速度可能要比用户的速度快,所以可以通过减速测试性能。
① props change时,如何重置组件内的state?(不使用useEffect)
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 Avoid: Resetting state on prop change in an Effect
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ This and any other state below will reset on key change automatically
const [comment, setComment] = useState('');
// ...
}
将组件分为两部分,在组件ProfilePage
中,userId 发生改变,React会重新创建DOM并重置组件以及其所有子组件Profile组件的state。
② props change时,修改部分state值
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🔴 Avoid: Adjusting state on prop change in an Effect
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Best: Calculate everything during rendering
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}
Subscribe React以外的数据(如第三方库、内置浏览器API)⭐
某些数据React跟踪不到。需要手动订阅,在useEffect内部实现。React有内置的Hook,用于订阅外部store,useSyncExternalStore
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
// ✅ Good: Subscribing to an external store with a built-in Hook
return useSyncExternalStore(
subscribe, // React won't resubscribe for as long as you pass the same function
() => navigator.onLine, // How to get the value on the client
() => true // How to get the value on the server
);
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
useSyncExternalStore(subscribe, getSnapshot,getServerSnapshot?)返回数据的快照,下面是参数
- subscribe— 接受单个callbac参数,并将其订阅到store上。当store更改时,应该调用callback,组件重新渲染,函数返回一个取消订阅函数
- getSnapshot — 用于检查订阅值自上次渲染以来是否发生了变化,因此需要引用稳定值。这意味着它要么需要是一个不可变的值,如字符串或数字,要么它需要是一个缓存/记忆的对象。
- optional getServerSnapshot — 返回store数据的初始快照的函数。两种情况下使用:
①在服务器上运行,生成HTML时;
②在客户端hydration期间,React获取服务器HTML并与其交互时
要确保客户端获取的值和服务器上返回的值完全相同。
前端hydration:一种将静态的React组件(由服务器渲染)转化成动态组件(由浏览器渲染)的过程。即将静态的html和css转化成客户端可交互的javascript组件的过程
如果每次渲染都传递不同的subscribe函数,那么会重新订阅。而在组件内部定义,就会导致每次重新渲染函数都不同:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// 🚩 Always a different function, so React will resubscribe on every re-render
function subscribe() {
// ...
}
}
应该将subscribe函数移到组件外部。或者使用useCallback仅在某些参数更改时重新订阅。
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ✅ Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);
}
如果某个逻辑必须在每次应用初次加载中运行一次,建议添加一个顶级变量跟踪它是否执行。
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ Only runs once per app load
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}
Event handles 是用户交互事件