React - useEffect

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 是用户交互事件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容