你真的需要用到useEffect吗?

Taro组件通信方式总结(最后一个是亮点) 别问为啥学 taro 问就是入坑了。 阅读详情

useEffect是React提供给我们的一个“逃生舱”,是React 的纯函数式世界通往命令式世界的“逃生通道”,选择合适的时机使用useEffect会让我们的代码既优雅又高效,反之会造成不必要的负担。

本文95%来自官方文档,均为官方推荐的写法或者做法。

你是不是也遇到过这个情况?

你希望状态name变化后,发起一次请求,于是你用了useEffect,这简直不要太适合:

useEffect(()=>{
  query();
},[name])

随着迭代,需求变更,name也会被其他地方给修改,但是他只是临时修改,并不需要发起一次请求。

幸运的是,你发现了这个问题,因为之前name相关的修改地方太多了,所以不敢动,于是你用if处理:

useEffect(()=>{
  if(isReFresh){
    query();
  }
},[name])

很好,没毛病,不会重复发起请求了。

后面需求又改了,现在age改变也要重新请求,很好,这不就是依赖数组的用处吗,加进去就好了:

useEffect(()=>{
  if(isReFresh){
    query();
  }
},[name,age])

随着时间推移,你逐渐发现:

  • 「是否发送请求」与「if条件」相关
  • 「是否发送请求」还与「name、age等依赖项」相关
  • 「name、age等依赖项」又与「很多需求」相关

根本分不清到底什么时候会发送请求,一直在堆屎…

Effect的生命周期

https://react.dev/learn/lifecycle-of-reactive-effects

Effect和其他的组件生命周期不太一样,组件的生命周期通常是:

  • 当组件被添加到屏幕上时 — mount
  • 当组件接收到新的props或者state时(通常是为了响应式交互) — update
  • 当组件从屏幕上被移除时 — unmount

这是设计组件生命周期的好办法,但是并不适用于Effect。Effect只能做两件事情:

  • 开始同步一些内容
  • 稍后停止同步

如果你的Effect取决于props和state的变化,那么这个循环可能会发生多次。

React也提供了相应的linter规则来检测你是否正确的指定了Effect的依赖,以保证你的Effect可以正确的同步最新的props和state。

为什么要少用Effect?

修改数据以更新渲染时:当你更新状态的时候,React会首先调用你的组件函数来计算屏幕上应该显示什么,然后React会将这些变化“提交”到DOM去渲染更新屏幕。接着React会执行你的Effects,如果你的Effects也马上更新了State,此时也将从头开始重新启动整个过程。

用户事件:当某一个数据变更时,在Effect运行时你可能无法准确的得知用户做了什么事情,只知道数据发生了变更。(依赖项变化->useEffect->if判断 ==> 开头提到的死循环问题)

在React官方文档的ESCAPE HATCHES章节中,我们发现effect占据了一半的文档,这个章节翻译成中文是逃生舱口,只有在危险的时候才会进入逃生舱口,effect亦是如此,只有在不得已的时候将其作为最后的逃生舱。

哪些情况可以不用Effect?

Don’t rush to add Effects to your components. Keep in mind that Effects are typically used to “step out” of your React code and synchronize with some external system. This includes browser APIs, third-party widgets, network, and so on. If your Effect only adjusts some state based on other state, you might not need an Effect.
不要急于向组件添加Effect。请记住,Effects 通常用于“跳出”React 代码并与某些外部系统同步,你可以把 Effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。这包括浏览器 API、第三方小部件、网络等。如果您的效果仅根据其他状态调整某些状态,则您可能不需要Effect。

从Effect-Usage的第二个小节开始,React官方就在说Effect只是一个逃生舱,你可能不需要用它,和Effect相关的文章,我感觉官方每时每刻都在重复强调少用,别用,换一个方法,就怕你滥用…

所以,大家可以从以下几个case开始考虑,是不是不要用useEffect了,但是阅读之前请注意,不要根据标题来断章取义的认为标题的这种情况就完全不适合用useEffect,可能不适合用useEffect的仅在内容描述的某一些case下,而其他的情况还需要再分析考虑。

一、依赖props或者state

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 🔴 Avoid:冗余的状态和不必要的Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

没必要这么写,既复杂又低效。这种情况就命中了我前面说的他会从头开始重新渲染整个页面。

function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Good: 重渲染期间计算
  const fullName = firstName + ' ' + lastName;
  // ...
}

当你需要计算某些值,而恰好计算的参数都是可以从Props或者State中获取的,不要把他放在State中,相对应的,可以在渲染期间计算它。这使得:

  • 代码更快(避免了额外的“级联”更新)
  • 更简单(删除了一些代码)
  • 更不容易出错(避免了由于不同状态变量彼此不同步而导致的错误)

非state数据是否可以?

答:不可以

这个就涉及到React本身的更新渲染机制,也就是我们为什么要用useState来set数据。如果不用useState,是不会触发React本身的更新的。

二、缓存昂贵的计算

如何判断本次计算是否昂贵?

一般而言,除非您要创建或遍历数千个对象,否则它可能并不昂贵。

如果记录的总时间加起来很大(比如 1 毫秒或更多),那么记住该计算可能是有意义的。

可以利用如下的方式来测算:

console.time('filter array');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter array');
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');

  // 🔴 Avoid: 冗余的状态和不必要的Effect
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}

与前面的示例一样,这既不必要又低效。

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ Good:当getFilteredTodos执行的不慢的时候,这个写法很好
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}

通常情况下,这种写法已经是很优秀的写法了!但是如果getFilteredTodos很慢,或者你有很多其他的待办事项会导致props或者state变化的非常的频繁。在这种情况下,如果你不想重新计算的话,可以将昂贵的计算包裹在useMemo的Hook中来缓存。

注意:useMemo 不会让第一次渲染更快。它只会帮助您跳过不必要的更新工作。

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ Good:除非 todos 或 filter 改变了,否则不会重新执行这个方法
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}

三、当props改变时重置所有state

这个 ProfilePage 组件接收一个 userId。该页面包含评论输入,您使用评论状态变量来保存其值。当您从一个配置文件导航到另一个配置文件时,评论状态不会重置。因此,很容易不小心在错误的用户个人资料上发表评论。所以你考虑用useEffect来清空state:

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // 🔴 Avoid: prop 改变时用useEffect来重置state
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

其实大家都可以感受得到,这个写法非常的低效且一看就不是正确的解法。如果你有多个state,每多一个state你都要加一次清空。而且ProfilePage 及其子项将首先使用过时值呈现,然后再次呈现清空后的值。并且如果这个组件是嵌套的,你也需要一个一个清除嵌套组件的state,显然不是最优解。

相反的,你可以用key来标识每一个组件,来告诉React,每一个用户的个人资料在概念上是不同的:

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
      />
  );
}

function Profile({ userId }) {
  // ✅ Good: comment包括其他的state在key改变时会自动的重置
  const [comment, setComment] = useState('');
  // ...
}

通常,React在同一组件的同一个地方渲染时,React会保留状态。

四、当props改变时重置部分state

可能也有一些场景,比如说你只希望在props改变时,只修改部分的state,你可能会想到用useEffect来清除部分,如下:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // 🔴 Avoid: prop 改变时用useEffect来重置state
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

这里的问题和第三点说的一致,而且setSelection(null)调用将导致再次重新渲染 List 及其子组件,再次重新启动整个过程。

第一步还是删除useEffect,考虑在渲染期间直接调整状态:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // 🟢 Better: 渲染改变时调整state
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

带着一些疑问来看一下这个写法:

  • 为什么要items !== prevItems,不写会怎么样?
  • 这种写法优势在哪里,好在哪里?
  • …

Storing information from previous renders [2]

  • 它必须在prevCount !== count之类的条件内,并且条件内必须有 setPrevCount(count)之类的调用。否则,您的组件重复循环渲染,直到它崩溃。此外,你只能像这样更新当前渲染组件的状态。在渲染过程中调用另一个组件的set函数是错误的。最后,你的 set 调用应该仍然更新状态而不是整体替换改变[3]。
  • 当您在渲染期间调用 set 函数时,React 将在您的组件以返回语句退出后立即重新渲染该组件,然后再渲染子组件。这样,子组件就不需要渲染两次。组件函数的其余部分仍将执行(结果将被丢弃)。

即使这种写法比useEffect更好,但是大部分组件也不会这么写,不管你怎么做,基于属性或其他状态调整状态都会使你的数据流更难理解和调试。仍然不建议用这个写法。遇到这类问题,我们还是优先考虑将这个问题归到其余的两个解法中来:

  • 能否重置所有状态
  • 在渲染中计算结果
function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  // ✅ Best: 在渲染期间计算一切
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}

这种写法更好,因为它的结果是计算出来的,每次渲染都会重新计算一次结果。对大多数项目遇到这种情况来说,这个显然是更优解。

五、在事件处理程序之间共享逻辑

假设你有一个带有两个按钮(购买和结帐)的产品页面,这两个按钮都可以让你购买该产品。你希望在用户将产品放入购物车时显示通知。在两个按钮的点击处理程序中调用 showNotification() 感觉是重复的,因此你可能想将此逻辑放在副作用中:

function ProductPage({ product, addToCart }) {
  // 🔴 Avoid: 将特定事件逻辑放在Effect中
  useEffect(() => {
    if (product.isInCart) {
      // 展示通知
      showNotification(`Added ${product.name} to the shopping cart!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }

  function handleCheckoutClick() {
    addToCart(product);
    navigateTo('/checkout');
  }
  // ...
}

在这里用useEffect是没必要的,而且可能会导致一些bug,例如,假设你的应用程序在页面重新加载之间“记住”了购物车。如果你将产品添加到购物车一次并刷新页面,通知将再次出现。每次你刷新该产品的页面时,它都会继续出现。这是因为 product.isInCart在页面加载时已经是 true,所以上面的effect将调用 showNotification()。

当您不确定某些代码是应该在 Effect 中还是在事件处理程序中时,问问自己为什么这段代码需要运行。只有当代码应该在组件被展示给用户再运行时,才应该使用 useEffect。在这个例子中,通知应该出现是因为用户按下了按钮,而不是因为页面被显示了!正确的应该是将对应的时间逻辑放在特定事件被处理时才触发:

function ProductPage({ product, addToCart }) {
  // ✅ Good: 特定事件逻辑只有在事件被处理时才触发
  function buyProduct() {
    addToCart(product);
    showNotification(`Added ${product.name} to the shopping cart!`);
  }

  function handleBuyClick() {
    buyProduct();
  }

  function handleCheckoutClick() {
    buyProduct();
    navigateTo('/checkout');
  }
  // ...
}

六、POST请求

useEffect会发起两次请求

useEffect(() => {
  logVisit(url); // Sends a POST request
}, [url]);

在dev环境中,logVisit 将针对每个 url 调用两次,因此你可能想尝试修复它。 我们建议保持此代码不变。 与前面的示例一样,运行一次和运行两次之间没有用户可见的行为差异。从实际的角度来看,logVisit 不应在开发中执行任何操作,因为你不希望来自开发机器的日志影响生产指标。每次你保存其文件时,你的组件都会remounts,因此无论如何它都会记录开发中的额外访问。

在production环境中,不会访问两次。

假设你有一个Form组件,会发送2组请求。一组是在组件mount的时候发起,另一种是在点击提交按钮时发起:

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // ✅ Good: This logic should run because the component was displayed
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);

  // 🔴 Avoid: Event-specific logic inside an Effect
  const [jsonToSubmit, setJsonToSubmit] = useState(null);
  useEffect(() => {
    if (jsonToSubmit !== null) {
      post('/api/register', jsonToSubmit);
    }
  }, [jsonToSubmit]);

  function handleSubmit(e) {
    e.preventDefault();
    setJsonToSubmit({ firstName, lastName });
  }
  // ...
}

我们可以分析一下这两个:

  • 第一个请求是合理的,因为触发请求的原因就是表单已经显示渲染,需要获取一次(在dev会触发两次)
  • 第二个不合理,第二个注册并不是由表单渲染而引起的请求,只是在某个特定条件下(比如:用户按下按钮发起请求),此时应该放在特定的事件请求方法中
function handleSubmit(e) {
  e.preventDefault();
  // ✅ Good: Event-specific logic is in the event handler
  post('/api/register', { firstName, lastName });
}

七、计算链

有时你可能会想链接副作用,每个副作用都根据其他状态调整一个状态:

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  // 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1)
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) {
      setIsGameOver(true);
    }
  }, [round]);

  useEffect(() => {
    alert('Good game!');
  }, [isGameOver]);

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    } else {
      setCard(nextCard);
    }
  }
}

如果用useEffect来处理的话,会有两个问题:

  • 低效:组件(及其子级)必须在链中的每个 set 调用之间重新渲染。最坏的情况下渲染是这样的:setCard→ 渲染 → setGoldCardCount → 渲染 → setRound → 渲染 → setIsGameOver → 渲染
  • 迭代能力和维护性差:假设后续有新需求,需要时空回溯,获取游戏的每个动作历史。你会通过将每个状态变量更新为过去的一个值来实现。然而,每次修改card就会触发一次Effect链并且更改你需要显示的数据,这种代码往往是僵硬且脆弱的。

在这种情况下,最好在渲染期间计算你能做什么,并在事件处理程序中调整状态:

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // ✅ Calculate what you can during rendering
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    }

    // ✅ Calculate all the next state in the event handler
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount <= 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert('Good game!');
        }
      }
    }
  }
}

这样效率更高。此外,如果打算做一个查看游戏历史的方法,现在能够将每个状态变量设置为过去的值,而不会触发调整每个其他值的Effect链。如果您需要在多个事件处理程序之间重用逻辑,您可以提取一个函数并从这些处理程序中调用它。

在事件处理内部,状态表现的会像快照。举个例子,在你调用setRound(round + 1)时,round值会同步的更新给用户,但是如果你想要在方法里面拿到更新后的值,需要像这样const nextRound = round + 1手动定义它。

说到这里,你如果觉得如果涉及到计算链,就先摒弃useEffect,那就错了。并不是所有的计算链都不适合在useEffect中。举个例子,如果说你有多个dropdown,且下一个dropdown的内容依赖于上一个dropdown选择的内容,此时用useEffect来处理计算链是合适的,因为此时我们的数据是和后端交互的,从服务端中获取数据,在这种情况下,由于获取到的数据是异步的,需要等待请求结果,所以用effect反而可以保证数据的同步性。这样做的好处是,无论哪一个下拉框的选项发生了变化,都会触发相应的 Effect 钩子函数,保证了整个表单的数据同步性。

八、初始化应用程序

一些逻辑应该只在应用程序加载时运行一次

你可能想将它放在顶层组件的副作用中:

function App() {
  // 🔴 Avoid: Effects with logic that should only ever run once
  useEffect(() => {
    loadDataFromLocalStorage();
    checkAuthToken();
  }, []);
  // ...
}

我想如果前面你有认真读这篇文章,你应该知道问题。前面提到了,在dev环境下,useEffect会触发两次。这就可能会导致问题,就以这个代码为例,可能会导致身份令牌验证失效,因为可能这个方法就是用来调用一次的,再次请求可能就拿不到身份信息了。

虽然这个问题只在dev环境中会被触发,但是还是要尽量保证一致性,便于调试和重用。如果某些逻辑必须在每次应用加载时运行一次而不是每次组件安装时运行一次,请添加一个顶级变量来跟踪它是否已经执行:

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      // ✅ Only runs once per app load
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

您还可以在模块初始化期间和应用程序呈现之前运行它:

if (typeof window !== 'undefined') { // Check if we're running in the browser.
  // ✅ Only runs once per app load
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}

尽量慎用此模式,即使这个组件最终没被渲染,当你的组件被导入时就会执行一次。应尽量将应用程序范围的初始化逻辑保留到根组件模块(如 App.js)或应用程序的入口点。

九、通知父级状态变更

假设你正在编写一个具有内部 isOn 状态的 Toggle 组件,该状态可以是 true 或 false。有几种不同的方式来切换它(通过单击或拖动)。你希望在 Toggle 内部状态发生变化时通知父级,因此你公开了一个 onChange 事件并从副作用中调用它:

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  // 🔴 Avoid: The onChange handler runs too late
  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange])

  function handleClick() {
    setIsOn(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      setIsOn(true);
    } else {
      setIsOn(false);
    }
  }

  // ...
}

和之前一样,这个做法并不理想,Toggle更新状态->React更新屏幕->React运行Effect->调用onChange->父组件更新状态->重渲染(这里要启动另一个渲染通道)。

如果可以的话,最好一次性完成所有事情。

function Toggle({ isOn, onChange }) {
  function handleClick() {
    onChange(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      onChange(true);
    } else {
      onChange(false);
    }
  }

  // ...
}

通过这种方法,Toggle 组件及其父组件都会在事件期间更新其状态。React 将来自不同组件的更新一起批量处理,因此只有一个渲染通道。

这个做法也叫“状态提升”,将状态提升到父组件中,让父组件通过切换状态来完全控制Toggle,这意味着父组件将必须包含更多逻辑,但总体上需要担心的状态会更少。每当您尝试保持两个不同的状态变量同步时,请尝试提升状态!

十、将数据传递给父级

function Parent() {
  const [data, setData] = useState(null);
  // ...
  return <Child onFetched={setData} />;
}

function Child({ onFetched }) {
  const data = useSomeAPI();
  // 🔴 Avoid: Passing data to the parent in an Effect
  useEffect(() => {
    if (data) {
      onFetched(data);
    }
  }, [onFetched, data]);
  // ...
}

这个操作是可以实现数据传递,不存在写法问题。但是在React中,数据是从父组件传递到子组件的单向数据流动。当你发现错误时,可以沿着组件链追踪信息的来源,直到你发现某个组件有错误属性或者状态。当子组件在副作用中更新其父组件的状态时,数据流变得很难追踪。由于子级和父组件都需要相同的数据,因此让父级获取该数据,然后将其传递给子级才是正确的做法:

function Parent() {
  const data = useSomeAPI();
  // ...
  // ✅ Good: Passing data down to the child
  return <Child data={data} />;
}

function Child({ data }) {
  // ...
}

十一、订阅外部存储

有时,你的组件可能需要订阅 React 状态之外的一些数据。此数据可能来自第三方库或内置浏览器 API。由于此数据可能会在 React 不知情的情况下发生变化,因此你需要手动为你的组件订阅它。这通常通过useEffect完成,例如:

function useOnlineStatus() {
  // Not ideal: Manual store subscription in an Effect
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);
  return isOnline;
}

function ChatIndicator() {
  const isOnline = useOnlineStatus();
  // ...
}

在这里,该组件订阅了一个外部数据存储(在本例中为浏览器 navigator.onLine API)。由于此 API 在服务器上不存在(因此它不能用于初始 HTML),因此最初将状态设置为 true。每当该数据存储的值在浏览器中发生变化时,组件都会更新其状态。

这么用其实也很正常,一般也不会挑什么毛病。但是,我们有更好的解决方案,React中有一个专门的用户订阅外部存储的钩子,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();
  // ...
}

与使用副作用手动将可变数据同步到 React 状态相比,这种方法更不容易出错。通常,你会像上面的 useOnlineStatus()一样编写一个自定义钩子,这样你就不需要在各个组件中重复此代码。

十二、获取数据

前面说了这么多可以不用的useEffect,我就当大家都认可了,但是大家心里肯定有一个是觉得一定要放在useEffect里面的,那就是获取数据

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);

  useEffect(() => {
    // 🔴 Avoid:没有清除逻辑的获取数据
    fetchResults(query, page).then(json => {
      setResults(json);
    });
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

肯定有人会质疑,这有啥问题?请求参数(query)或者页码(page)改变发起一次请求,不就该放在useEffect里面吗?

但是我现在要说的是,你不需要把这个逻辑放在事件处理函数中。

这可能看起来与之前需要将逻辑放入事件处理函数中的示例相矛盾!但是,请考虑到打字事件并不是获取数据的主要原因。搜索输入框的值经常从 URL 中预填充,用户可以在不关心输入框的情况下导航到后退和前进页面。

page 和query 的来源其实并不重要。只要该组件可见,你就需要通过当前 page 和 query 的值,保持 results 和网络数据的同步。这就是为什么这里是一个 Effect 的原因。

然而,大家在写这类请求的时候一定会碰到一个问题,那就是用户快速输入,会频繁发起请求,但是无法保证返回的数据顺序。由于setState是最后调用的,将会错误地显示结果。这种情况被称为 “竞态条件”:两个不同的请求 “相互竞争”,并以与你预期不符的顺序返回。

你可能会考虑加上debounce,或者加上清理函数来忽略较早的返回结果。这确保了当你在 Effect 中获取数据时,除了最后一次请求的所有返回结果都将被忽略。

处理竞态条件并不是实现数据获取的唯一难点。你可能还需要考虑:

  • 缓存响应结果(使用户点击后退按钮时可以立即看到先前的屏幕内容)
  • 如何在服务端获取数据(使服务端初始渲染的 HTML 中包含获取到的内容而不是加载动画)
  • 以及如何避免网络瀑布(使子组件不必等待每个父组件的数据获取完毕后才开始获取数据)

如果你不使用框架(也不想开发自己的框架),但希望使从 Effect 中获取数据更符合人类直觉,请考虑像这个例子一样,将获取逻辑提取到一个自定义 Hook 中:

function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(url)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setData(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [url]);
  return data;
}

function SearchResults({ query }) {
  const [page, setPage] = useState(1);
  const params = new URLSearchParams({ query, page });
  const results = useData(`/api/search?${params}`);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

一般来说,当你不得不编写 Effect 时,请留意是否可以将某段功能提取到专门的内置 API 或一个更具声明性的自定义 Hook 中,比如上面的 useData。你会发现组件中的原始 useEffect 调用越少,维护应用将变得更加容易。

那我应该在什么时候用Effect?

https://react.dev/reference/react/useEffect#usage

内容太多,大家可以作为后续自己学习的内容…

回到开始

当你希望name变化之后发起请求,你首先应该问自己:

「状态name变化,接下来需要发起请求」

还是

「某个用户行为需要发起请求,请求依赖状态name作为参数」

如果是后者,我们应当将这个请求和用户的操作关联,而不是放在useEffect当中。

<Button onClick={(e)=>{query(e)}}>点击发起请求</Button>

经过这样修改,「状态name变化」与「发送请求」之间不再有因果关系,后续对状态name的修改不会再有「无意间触发请求」的顾虑。

诸如此类的问题,大家期望使用useEffect的时候,都应该考虑一下,是否在前面提到的十二个case其中之一,是否有更好的写法。

总结

useEffect是React提供给我们的一个“逃生舱”,是React 的纯函数式世界通往命令式世界的“逃生通道”,选择合适的时机使用useEffect会让我们的代码既优雅又高效,反之会造成不必要的负担。

以下情况可能考虑不要使用useEffect

  • 如果你可以在渲染期间计算某些内容,则不需要使用 Effect。
  • 想要缓存昂贵的计算,请使用 useMemo 而不是 useEffect。
  • 想要重置整个组件树的 state,请传入不同的 key。
  • 想要在 prop 变化时重置某些特定的 state,请在渲染期间处理。
  • 组件 显示 时就需要执行的代码应该放在 Effect 中,否则应该放在事件处理函数中。
  • 如果你需要更新多个组件的 state,最好在单个事件处理函数中处理。
  • 当你尝试在不同组件中同步 state 变量时,请考虑状态提升。
  • 你可以使用 Effect 获取数据,但你需要实现清除逻辑以避免竞态条件。

参考:

[1] https://react.dev/learn/thinking-in-react#step-3-find-the-minimal-but-complete-representation-of-ui-state

[2] https://react.dev/reference/react/useState#storing-information-from-previous-renders

[3] https://react.dev/reference/react/useState#updating-objects-and-arrays-in-state

[4] https://react.dev/learn/state-as-a-snapshot

[5] https://react.dev/learn/queueing-a-series-of-state-updates

[6] https://react.dev/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development

[7] https://react.nodejs.cn/reference/react/useSyncExternalStore

深入理解 React 的 useEffect:全面指南 在 React 中,useEffect是一个非常重要的 Hook,用于在函数组件中处理。它强大而灵活,是函数组件中替代类组件生命周期方法的核心工具。通过useEffect本篇文章将从基础到进阶,全面解析useEffect的用法及最佳实践。useEffect是 React 提供的一个 Hook,用于处理函数组件中的。副作用的概念简单来说就是那些与渲染无关的逻辑,比如访问浏览器 API、订阅数据流、定时器等。useEffectuseEffect的第二个参数是一个,用于控制它的执行时机。 阅读详情

相关推荐

ESP32Servo库驱动SG90舵机:从入门到精准控制

本文详细介绍了如何使用ESP32Servo库驱动SG90舵机,从硬件连接到软件环境搭建,再到编写示例程序实现精准控制。内容涵盖库安装、基础驱动代码解析、舵机校准方法以及平滑运动控制,帮助开发者快速掌握ESP32控制舵机的核心技能,并提供了电源管理、信号抗干扰等实用避坑指南。

ik678901的博客 898

React核心 -- React-Hooks

hooks 存在的意义 hooks 之间的状态是独立的,有自己独立的上下文,不会出现混淆状态的情况 让函数有了状态管理 解决了 组件树不直观、类组件难维护、逻辑不易复用的问题 避免函数重复执行的副作用 应用场景 利用 hooks 取代生命周期函数 让组件有了状态 组件辅助函数 处理发送请求 存取数据 做好性能优化 hooks API 从 react 中引入 1. useState 给函数组件添加状态 初始化以及更新组件状态 const [count, setCount] = R.

小丞同学 849

1.3-----Simplify 3D切片软件:从新手困惑到高效打印的进阶设置指南

本文详细介绍了Simplify 3D切片软件在大型模型打印中的高效设置技巧,包括切片效率、支撑结构智能生成和精细参数调节等核心优势。通过实战案例和进阶参数设置指南,帮助用户从新手快速进阶到高效打印,显著提升3D打印质量和效率。

weixin_42552716的博客 422

useEffect使用详解

useEffect是React中的一个钩子函数,用于处理副作用操作。副作用是指在组件渲染过程中,可能会对外部环境产生影响的操作,比如数据获取、订阅事件、操作DOM等。useEffect接受两个参数:一个是副作用函数,另一个是依赖数组。副作用函数会在组件渲染时执行,并且在每次组件更新后也会执行。如果依赖数组不为空,并且依赖数组中的值发生变化时,副作用函数也会被重新执行。如果依赖数组为空,则副作用函数只会在组件渲染时执行一次。以下是useEffect。

一步一个脚印,踏实努力,向着目标前进 1万+

微信小游戏CE修改实战:从入门到精通(附防作弊思路)

本文深入探讨微信小游戏的安全攻防实战,重点分析CE修改、反编译和重打包等安全风险。通过详细的内存修改案例和代码保护方案对比,提供从客户端到服务端的全方位防护策略,帮助开发者构建有效的防作弊体系。

m2n3b4v5c6的博客 1145

CTF-Web学习笔记:服务端请求伪造(SSRF)篇

本文系统介绍了CTF比赛中SSRF漏洞的攻防技术。首先解析了SSRF的核心原理,即利用服务器对用户输入URL的未校验缺陷,诱导其访问内网资源。然后详细分析了5类常见CTF题型:文件读取、内网探测、云服务元数据窃取、协议绕过及组合漏洞利用。重点讲解了绕过URL过滤、利用特殊协议(如dict://、gopher://)等实战技巧,并通过一道SSRF+Redis的高阶题目演示完整攻击链。最后给出防御建议:严格校验输入URL、禁用危险协议、实施内网隔离等。

Deng7326的博客 1704

taro小程序的生命周期

taro小程序的生命周期

肖肖肖丽珠的博客 1455

C++笔试题整理

目录 1. 笔试题1 1.1 链表反转 1.2 String 2. 笔试题2 2.1 求下面函数的返回值(微软) 2.2 什么是“引用”?申明和使用“引用”要注意哪些问题? 2.3 将“引用”作为函数参数有哪些特点? 2.4 在什么时候需要使用“常引用”?  2.5 将“引用”作为函数返回值类型的格式、好处和需要遵守的规则? 2.6 “引用”与多态的关系? 2.7 “引用”与指针的区别是什么? 2.8 什么时候需要“引用”? 2.9 结构与联合有和区别? 2.10 下面关于.

星月相随的专栏 2484

react - useEffect函数的理解和使用

React的useEffect Hook可以替代类组件的生命周期方法,它能在函数组件中执行副作用操作。useEffect接收两个参数:回调函数和依赖项数组。根据第二个参数的不同,useEffect会有不同的表现:不传参数会监听所有状态变化;空数组相当于componentDidMount;指定状态则只监听该状态;返回回调函数相当于componentWillUnmount。useEffect可以在组件中多次使用,建议将不同业务逻辑放在不同的useEffect中处理。需要注意的是,无论参数如何,useEffect

尽日寻春不见春,芒鞋踏遍陇头云。归来笑拈梅花嗅,春在枝头已十分。 9341

基于分水岭分割进行肺癌诊断

近年来,肺癌的发病率和病死率均迅速上升,目前已居所有癌症之首。随着肺癌病人数量的增加,医生对肺部 CT 图像进行研判的工作量也增加了不少,在这种情况下难免工作效率降低甚至会出现误诊。为了帮助医生减少重复性工作,对肺部 CT 图像进行计算机辅助检测的技术就被广泛应用于对肺癌的诊断和治疗过程中。医学 CT 图像处理主要是研究医学图像中的器官和组织之间的关系,并进行病理性分析。

在职AI算法工程师,擅长计算机视觉,YOLO目标检测、分割等,擅长web、pyqt界面可视化,好内容持续更新中,来这里跟大家一起学习,共同进步 1568

UCIE Specification详解(八)

Logical Physical Layer(逻辑物理层)

Crazzy_M的博客 1608

你真的用对 useEffect 了吗?

点击上方前端瓶子君,关注公众号回复算法,加入前端编程面试算法每日一题群最近在公司搬砖的过程中遇到了一个bug,页面加载的时候会闪现一下,找了很久才发现是useeffect的依赖项的问题,...

我本可以容忍阳光,如果我不曾见过太阳,然而阳光已使我荒凉,成为更新的荒凉。 2159

西南科技大学模拟电子技术实验二(二极管特性测试及其应用电路)预习报告

3、用二极管正向虚拟IV仪测出数据,并记录数据于虚表3和虚表4。1、按照指导书multisim仿真软件上画出三种模型电路图,1、按照指导书multisim仿真软件上画出三种模型电路图,1、按照指导书multisim仿真软件上画出三种模型电路图,表6 二极管三种模型参数测试表(电源电压=1.2V)2、用万用表测出数据,并记录数据于虚表1和虚表2。2、用万用表测出数据,并记录数据于虚表5和虚表6。表5 二极管三种模型参数测试表(电源电压=12V)二极管三种模型参数测试表(电源电压=12V)

Welcome To My6n Blog 4082

用了这么久React,你真的搞懂useEffect了吗?

简单来说,useEffect就是React函数组件中处理"副作用"的利器。什么是副作用?就是那些会影响组件外部世界的操作,比如数据获取、设置订阅、手动修改DOM等。在类组件时代,我们通常在componentDidMount、componentDidUpdate和componentWillUnmount这些生命周期方法中处理这些操作。但现在有了函数组件和Hooks,useEffect就能帮我们统一处理所有这些场景。// 类似于componentDidMount和componentDidUpdate。

在0与1的裂缝里,种出AI的未来。 1058

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

react 使用 useEffect 及踩坑

useEffect时reactHook中最重要,最常用的hook之一。useEffect相当于react中的什么生命周期呢?这个问题在react官网中有过介绍,在使用的过程中,容易被忽略,在面试的时候却经常被问及,(面试造航母,上班拧螺丝?

秋来九月八 3万+

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

ReactHook - useEffect、useLayoutEffect

一. useEffect 作用: 推迟代码执行 渲染完成之后再执行,常用于 数据读取 完成移除 定时器、监听 第二个参数来模拟生命周期。 useEffect存在一种bail out(保释)机制: 当你尝试设置相同的状态,不会渲染,也不会触发effect 设置了相同state: “补救”——仅仅让你把函数执行完,不会真的渲染、也不会执行effect 又设置了相同state——没有 setState state设置重复的值——只会重复渲染一次 +1 例如: useEffect——cl

EmotionComputer 1530

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

【React】945- 你真的用对 useEffect 了吗?

最近在公司搬砖的过程中遇到了一个bug,页面加载的时候会闪现一下,找了很久才发现是useeffect的依赖项的问题,所以打算写篇文章总结一下,希望对看到文章的你也有所帮助。1.什么是use...

pingan8787 1299

useEffect

主要总结了useEffect的基本语法、核心原理、使用模式、高级用法、性能优化等内容。

热爱前端的大宝同学。 1554

React useEffect

React 的生命周期方法控制着组件从创建到销毁的整个过程,函数组件中通过 useEffect。,你可以轻松管理 React 组件中的数据获取、事件订阅、计时器等副作用逻辑!实现类似类组件的生命周期方法。以下是关于 React 生命周期和。的一个重要特性,例如在设置计时器或订阅事件时。在函数组件中,这些生命周期通过。可以在同一个组件中使用多个。处理不同的副作用逻辑。

weixin_42548252的博客 1139

useEffect 完整指南

你用Hooks写了一些组件,甚或写了一个小型应用。你可能很满意,使用它的API很舒服并且在这个过程中获得了一些小技巧。你甚至可能写了一些custom Hooks去抽离重复的逻辑(精简掉了300行代码),并且得意地展示给你的同事看,“干得漂亮”,他们如是说。 但有时候当你使用useEffect你总觉得哪儿有点不对劲。你会嘀咕你可能遗漏了什么。它看起来像class的生命周期…但真的是这样吗?你发觉...

大灰狼的小绵羊哥哥的博客 7224

hooks中useEffect()使用总结

常见使用: 获取数据案例: import React, { useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); useEffect(() => { const fetchData = async () => { const result = await axios

庚中 3万+

useEffect详解

useEffect使用时有以下4种情况 1、不传递 useEffect不传递第二个参数会导致每次渲染都会运行useEffect。然后,当它运行时,它获取数据并更新状态。然后,一旦状态更新,组件将重新呈现,这将再次触发useEffect,这就是问题所在。 //1 useEffect(()=>{ console.log(props.number) setNumber(props.number) }) //所有更新都执行 2、传递空数组 //2 useEffect(()=>{ console.log(

IT_bar的博客 5万+

React中的useEffect

这个依赖项数组的目的是帮助 Reac t优化性能,确保副作用函数仅在依赖项发生变化时执行,而不是在每次渲染时都执行。清除副作用的逻辑,写在 useEffect 内回调函数的 return 后面,需要注意的是,return 后必须是一个函数!如果省略依赖项数组,副作用函数将在每次组件重新渲染时都执行,这可能导致性能问题和不必要的副作用。如果依赖项数组不为空,副作用函数将在组件挂载时执行一次,并在依赖项发生变化时执行。如果依赖项数组为空,副作用函数仅在组件挂载和卸载时执行,相当于类似于类组件中的。

693

一份完整的useEffect指南

本文太长,谨慎阅读。 本文原文:一份完整的useEffect指南 原文档: A Complete Guide to useEffect 你使用hooks写了一些组件。也许只是一个小的应用。你基本上是满意的。你对这个API游刃有余,并在此过程中学会了一些技巧。你甚至做了一些自定义Hook来提取重复逻辑(300行代码消失了!)并向你的同事展示了它。他们会说,“干得好”。 但是有时候当你使用useE...

xiaohesong 1万+

react hooks--useEffect

React 在构建用户界面整体遵循函数式的编程理念,即固定的输入有固定的输出,尤其是在推出函数式组件之后,更加强化了组件纯函数的理念。但实际业务中编写的组件不免要产生请求数据、订阅事件、手动操作 DOM 这些副作用 effect ,这样难免让函数组件变得不那么纯,于是 React 提供 useEffect 和 useLayoutEffect 这样的 hook,给开发者提供专门管理副作用的方式。◼。

小白小白从不日别的博客 1576
上一篇: Antd DatePicker日期选择,如何禁止选择之前的时间
下一篇: 管理系统基建配置
知夏__
博客等级 码龄5年 94粉丝 18原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值