【问题标题】:With useEffect, how can I skip applying an effect upon the initial render?使用 useEffect,如何跳过在初始渲染时应用效果?
【发布时间】:2019-04-10 06:28:10
【问题描述】:

使用 React 的新效果挂钩,如果某些值在重新渲染之间没有改变,我可以告诉 React 跳过应用效果 - React 文档中的示例:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

但上面的示例将效果应用于初始渲染,以及count 已更改的后续重新渲染。如何告诉 React 跳过初始渲染的效果?

【问题讨论】:

  • 你看过React.useMemo吗?

标签: reactjs react-hooks


【解决方案1】:

如指南所述,

Effect Hook,useEffect,增加了从函数组件执行副作用的能力。它的用途与 React 类中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 相同,但统一为一个 API。

在本指南的示例中,预计 count 仅在初始渲染时为 0:

const [count, setCount] = useState(0);

所以它将作为componentDidUpdate 工作并进行额外检查:

useEffect(() => {
  if (count)
    document.title = `You clicked ${count} times`;
}, [count]);

这基本上是可以用来代替useEffect 的自定义钩子的工作方式:

function useDidUpdateEffect(fn, inputs) {
  const didMountRef = useRef(false);

  useEffect(() => {
    if (didMountRef.current) { 
      return fn();
    }
    didMountRef.current = true;
  }, inputs);
}

感谢 @Tholle 建议 useRef 而不是 setState

【讨论】:

  • useRef 优于 setState 的建议在哪里?我在此页面上没有看到它,我想了解原因。
  • @rob-gordon 这是更新答案后删除的评论。原因是 useState 会导致不必要的组件更新。
  • 这种方法有效,但它违反了 react-hooks/exhaustive-deps linter 规则。 IE。 fn 未在 deps 数组中给出。有人有不违反 React 最佳实践的方法吗?
  • @JustinLang React linter rules != 最佳实践,他们只尝试解决常见的钩子问题。 ESLint 规则并不智能,可能会导致误报或误报。只要钩子背后的逻辑是正确的,就可以使用 eslint-disable 或 eslint-disable-next 注释安全地忽略规则。在这种情况下,fn 不应作为输入提供。请参阅说明,reactjs.org/docs/…。如果fn 内部的某些东西引入了deps,则更像是应该直接将它们提供为inputs
  • 注意:如果您使用多个 useEffects 检查 didMountRef,请确保只有最后一个(底部)将 didMountRef 设置为 false。 React 按顺序遍历 useEffects!
【解决方案2】:

这是一个自定义钩子,它只提供一个布尔标志来指示当前渲染是否是第一个渲染(当组件被安装时)。它与其他一些答案大致相同,但您可以在 useEffect 或渲染函数或您想要的组件中的任何其他位置使用标志。也许有人可以提出一个更好的名字。

import { useRef, useEffect } from 'react';

export const useIsMount = () => {
  const isMountRef = useRef(true);
  useEffect(() => {
    isMountRef.current = false;
  }, []);
  return isMountRef.current;
};

你可以像这样使用它:

import React, { useEffect } from 'react';

import { useIsMount } from './useIsMount';

const MyComponent = () => {
  const isMount = useIsMount();

  useEffect(() => {
    if (isMount) {
      console.log('First Render');
    } else {
      console.log('Subsequent Render');
    }
  });

  return isMount ? <p>First Render</p> : <p>Subsequent Render</p>;
};

如果你有兴趣,这里有一个测试:

import { renderHook } from '@testing-library/react-hooks';

import { useIsMount } from '../useIsMount';

describe('useIsMount', () => {
  it('should be true on first render and false after', () => {
    const { result, rerender } = renderHook(() => useIsMount());
    expect(result.current).toEqual(true);
    rerender();
    expect(result.current).toEqual(false);
    rerender();
    expect(result.current).toEqual(false);
  });
});

我们的用例是隐藏动画元素,如果初始道具表明它们应该被隐藏。如果道具发生变化,在以后的渲染中,我们确实希望元素动画出来。

【讨论】:

  • 导入从react-hooks-testing-library更改为@testing-library/react-hooksgithub.com/testing-library/react-hooks-testing-library/releases/…
  • 为什么选择“isMount”而不是“didMount”或“isMounted”?
  • 猜猜我在考虑当前渲染是否是发生安装的渲染。是的,我同意,现在回到它听起来有点奇怪。但它在第一次渲染时是真的,之后是假的,所以你的建议听起来会产生误导。 isFirstRender 可以工作。
  • 感谢钩子!我同意@ScottyWaggoner,isFirstRender 是一个更好的名字
  • 添加测试是 FIRE!反响很好。
【解决方案3】:

我找到了一个更简单的解决方案,不需要使用另一个钩子,但是它有缺点。

useEffect(() => {
  // skip initial render
  return () => {
    // do something with dependency
  }
}, [dependency])

这只是一个例子,如果您的情况很简单,还有其他方法可以做到这一点。

这样做的缺点是不能产生清理效果,只会在依赖数组第二次发生变化时执行。

不建议使用,您应该使用其他答案所说的内容,但我只是在这里添加了这个,所以人们知道这样做的方法不止一种。

编辑:

为了更清楚,您不应该使用这种方法来解决问题中的问题(跳过初始渲染),这仅用于教学目的,表明您可以做到同一件事以不同的方式。 如果您需要跳过初始渲染,请使用其他答案的方法。

【讨论】:

  • 我刚刚学到了一些东西。我不认为这会起作用,然后我尝试了它,它确实起作用了。谢谢!
  • React 应该为此提供更好的方法,但是这个问题在 Github 上是开放的,并且建议自己编写一个自定义解决方案(这完全是无意义的)。
  • 那个的问题是它也会在unMount上执行,并不完美。
  • @ncesar 如果这是唯一有效的答案,可能你做错了什么,因为另一个答案是正确的,而我的只是为了教学目的
【解决方案4】:

我使用常规状态变量而不是 ref。

// Initializing didMount as false
const [didMount, setDidMount] = useState(false)

// Setting didMount to true upon mounting
useEffect(() => { setDidMount(true) }, [])

// Now that we have a variable that tells us wether or not the component has
// mounted we can change the behavior of the other effect based on that
const [count, setCount] = useState(0)
useEffect(() => {
  if (didMount) document.title = `You clicked ${count} times`
}, [count])

我们可以像这样将 didMount 逻辑重构为自定义钩子。

function useDidMount() {
  const [didMount, setDidMount] = useState(false)
  useEffect(() => { setDidMount(true) }, [])

  return didMount
}

最后,我们可以像这样在我们的组件中使用它。

const didMount = useDidMount()

const [count, setCount] = useState(0)
useEffect(() => {
  if (didMount) document.title = `You clicked ${count} times`
}, [count])

更新使用 useRef 挂钩来避免额外的重新渲染(感谢@TomEsterez 的建议)

这次我们的自定义钩子返回一个函数,返回我们的 ref 的当前值。你也可以直接使用 ref,但我更喜欢这个。

function useDidMount() {
  const mountRef = useRef(false);

  useEffect(() => { mountRef.current = true }, []);

  return () => mountRef.current;
}

用法

const MyComponent = () => {
  const didMount = useDidMount();

  useEffect(() => {
    if (didMount()) // do something
    else // do something else
  })

  return (
    <div>something</div>
  );
}

顺便说一句,我从来没有使用过这个钩子,而且可能有更好的方法来处理这个问题,这将更符合 React 编程模型。

【讨论】:

  • useRef 更适合这种情况,因为useState 会导致组件的额外且无用的渲染:codesandbox.io/embed/youthful-goldberg-pz3cx
  • 您的useDidMount 函数中有错误。您必须用大括号 { ...}mountRef.current = true 封装在 useEffect() 中。离开他们就像写return mountRef.current = true,这会导致像An effect function must not return anything besides a function, which is used for clean-up. You returned: true这样的错误
【解决方案5】:

一个 TypeScript 和 CRA 友好的钩子,将其替换为 useEffect,这个钩子的工作方式类似于 useEffect,但不会在第一次渲染发生时触发。

import * as React from 'react'

export const useLazyEffect:typeof React.useEffect = (cb, dep) => {
  const initializeRef = React.useRef<boolean>(false)

  React.useEffect((...args) => {
    if (initializeRef.current) {
      cb(...args)
    } else {
      initializeRef.current = true
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, dep)
}

【讨论】:

    【解决方案6】:

    这是我基于 Estus Flask 的 answer 用 Typescript 编写的实现。它还支持清理回调。

    import { DependencyList, EffectCallback, useEffect, useRef } from 'react';
    
    export function useDidUpdateEffect(
      effect: EffectCallback,
      deps?: DependencyList
    ) {
      // a flag to check if the component did mount (first render's passed)
      // it's unrelated to the rendering process so we don't useState here
      const didMountRef = useRef(false);
    
      // effect callback runs when the dependency array changes, it also runs
      // after the component mounted for the first time.
      useEffect(() => {
        // if so, mark the component as mounted and skip the first effect call
        if (!didMountRef.current) {
          didMountRef.current = true;
        } else {
          // subsequent useEffect callback invocations will execute the effect as normal
          return effect();
        }
      }, deps);
    }
    

    现场演示

    下面的现场演示演示了 useEffectuseDidUpdateEffect 钩子之间的区别

    【讨论】:

      【解决方案7】:

      让我给你介绍react-use

      想跑:

      仅在第一次渲染后useUpdateEffect

      只有一次useEffectOnce

      是第一次挂载吗? useFirstMountState

      想要通过深度比较浅比较节流运行效果?更多here

      不想安装库?检查code 并复制。 (也可以为那里的好人提供star

      【讨论】:

        【解决方案8】:

        以下解决方案与上面类似,只是我更喜欢更清洁的方式。

        const [isMount, setIsMount] = useState(true);
        useEffect(()=>{
                if(isMount){
                    setIsMount(false);
                    return;
                }
                
                //Do anything here for 2nd render onwards
        }, [args])
        

        【讨论】:

        • 这不是一个理想的方法,因为设置状态会导致另一个渲染
        【解决方案9】:

        我打算对当前接受的答案发表评论,但空间不足!

        首先,在使用功能组件时,不要考虑生命周期事件,这一点很重要。考虑道具/状态的变化。我遇到过类似的情况,我只希望在特定道具(在我的情况下为 parentValue)从其初始状态更改时触发特定的 useEffect 函数。因此,我创建了一个基于其初始值的 ref:

        const parentValueRef = useRef(parentValue);
        

        然后在useEffect fn 的开头包含以下内容:

        if (parentValue === parentValueRef.current) return;
        parentValueRef.current = parentValue;
        

        (基本上parentValue没有变化就不要运行效果了,如果有变化就更新ref,准备下一次检查,继续运行效果)

        因此,尽管建议的其他解决方案可以解决您提供的特定用例,但从长远来看,它将有助于改变您对功能组件的看法。

        将它们视为主要基于某些道具渲染组件。

        如果您确实需要一些本地状态,那么 useState 会提供,但不要假设您的问题将通过存储本地状态来解决。

        如果你有一些代码会在渲染过程中改变你的道具,这个“副作用”需要被包裹在 useEffect 中,但这样做的目的是获得一个不受渲染时发生变化。 useEffect 钩子将在渲染完成后运行,并且正如您所指出的,它在每次渲染时运行 - 除非第二个参数用于提供道具/状态列表以识别哪些更改的项目将导致它将在以后运行。

        祝您在功能组件/钩子之旅上好运!有时有必要忘记一些东西来掌握一种新的做事方式:) 这是一本优秀的入门书:https://overreacted.io/a-complete-guide-to-useeffect/

        【讨论】:

          猜你喜欢
          • 2020-01-11
          • 1970-01-01
          • 2019-04-14
          • 1970-01-01
          • 1970-01-01
          • 2021-12-15
          • 2020-04-24
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多