当我只想在挂载时运行一次时,useEffect 缺少依赖项

Posted

技术标签:

【中文标题】当我只想在挂载时运行一次时,useEffect 缺少依赖项【英文标题】:useEffect has missind dependencies when I want to run it always just once on mount 【发布时间】:2020-05-21 06:53:57 【问题描述】:

我有一个组件应该从组件中读取一个属性(它是字符串“fill”或字符串“stroke”)并从对象中提取相应的键来读取它的上下文。

一旦选择了一个对象,就会安装它,访问活动对象并将其颜色作为填充颜色或描边颜色拉出。

useEffect(() => 
    setButtonColor(context.objects.getActiveObject()[props.type]);
, []); //on mount

像这样安装它:

<ColorPicker type="fill" />
<ColorPicker type="stroke" />

这应该只在装载时运行一次。我认为当 dep 数组为空时,它会在任何情况下运行一次。

那么我如何使用道具和上下文在装载时运行一次?

当我希望它在挂载时始终只运行一次时,为什么它需要一个依赖项,无论如何?

【问题讨论】:

您已经拥有的效果确实只会在每个挂载中使用的每个组件运行一次 “我想当 dep 数组为空时,它会在任何情况下运行一次。” 是的,这是正确的。它应该按您的预期工作,因此尚不清楚问题所在。但是,请注意,如果使用不同的 prop 值重新渲染组件,则代码不会运行。这似乎不是正确的行为。因此,为什么您应该将 prop 添加为依赖项。不知道上下文。 这能回答你的问题吗? How to call loading function with React useEffect only once @MuhammadZeeshan 第一个答案正是我在这里所做的,这使得它抱怨缺少依赖项。 @DanPantry 是的,这就是我们想要的效果。该组件使用颜色选择器更改所选对象的颜色。取消选择对象时,组件将被卸载。再次选择它时,我想从对象中获取设置的颜色并用它初始化颜色按钮。因此,从对象中提取颜色应该只在安装时运行一次。 【参考方案1】:

最好不要认为效果会在组件生命周期的特定时间点运行。虽然这是真的,但可以帮助您更好地掌握钩子的模型是,依赖数组是效果 同步 的事物列表:也就是说,每次都应该运行效果那些事情会改变。

当你得到一个 linter 错误表明你的依赖数组缺少 props 时,linter 试图告诉你的是你的效果(或回调,或记忆函数)依赖于不稳定的值。它这样做是因为通常这是一个错误。考虑以下几点:

function C( onSignedOut ) 
  const onSubmit = React.useCallback(() => 
    const response = await fetch('/api/session',  method: 'DELETE' )
    if (response.ok) 
      onSignedOut()
    
  , [])

  return <form onSubmit=onSubmit>
    <button type="submit">Sign Out</button>
  </form>

linter 将对onSubmit 中的依赖数组发出警告,因为onSubmit 依赖于onSignedOut 的值。如果您保留此代码原样,则onSubmit 将只创建一次,第一个值为onSignedOut。如果 onSignedOut 属性发生变化,onSubmit 将不会反映这种变化,您最终会得到对 onSignedOut 的过时引用。这在此处得到了最好的证明:

import  render  from "@testing-library/react"

it("should respond to onSignedOut changes correctly", () => 
  const onSignedOut1 = () => console.log("Hello, 1!")
  const onSignedOut2 = () => console.log("Hello, 2!")
  const  getByText, rerender  = render(<C onSignedOut=onSignedOut1 />)
  getByText("Sign Out").click()
  // stdout: Hello, 1!
  rerender(<C onSignedOut=onSignedOut2 />)
  getByText("Sign Out").click()
  // stdout: Hello, 1!
)

console.log() 语句不会更新。对于这个可能会违反您作为组件使用者的期望的特定示例。


现在让我们看看你的代码。

如您所见,此警告实质上是在说明您的代码可能没有按照您的想法执行。如果您确定自己知道自己在做什么,那么消除警告的最简单方法是禁用该特定行的警告。

useEffect(() => 
    setButtonColor(context.objects.getActiveObject()[props.type]);
    // eslint-disable-next-line react-hooks/exhaustive-deps
, []); //on mount

正确的做法是将依赖项放在数组中。

const  type  = props
useEffect(() => 
    setButtonColor(context.objects.getActiveObject()[type]);
, [context, type]);

但是,每次type 更改时,这都会更改按钮颜色。这里有一点需要注意:您正在设置状态以响应道具的变化。这称为派生状态。

希望在初始安装时设置该状态。由于您只想在初始安装时设置它,您可以简单地将您的值传递给React.useState(initialState),这将完全实现您想要的:

function C( type ) 
  const initialColor = context.objects.getActiveObject()[type];
  const [color, setButtonColor] = React.useState(initialColor);
  ...

这仍然会留下一个问题,即当您更改道具时,消费者可能会对为什么视图会更新感到困惑。 功能组件起飞之前很常见的约定(我仍然使用的一个)是使用 initial 一词为未监视更改的道具添加前缀:

function C( initialType ) 
  const initialColor = context.objects.getActiveObject()[initialType];
  const [color, setButtonColor] = React.useState(initialColor);

您仍然应该在这里小心:这确实意味着,在C 的生命周期内,它只会从contextinitialType 读取一次。如果上下文的值发生变化怎么办?您最终可能会在&lt;C /&gt; 中得到陈旧的数据。您可能可以接受,但值得一提。


React.useRef() 确实是一个很好的解决方案,可以通过仅捕获初始版本来稳定值,但对于这个用例来说不是必需的。

【讨论】:

相关:reacttraining.com/blog/…【参考方案2】:

这是我解决此问题的方法:

将颜色设置为变量,然后使用该变量在组件挂载时设置按钮颜色。

const oldColor = useRef(context.objects.getActiveObject()[props.type]);

useEffect(() => 
    setButtonColor(oldColor.current);
, []); //on mount

useRef 返回一个可变的 ref 对象,其 .current 属性为 初始化为传递的参数(initialValue)。返回的对象 将在组件的整个生命周期内持续存在。

【讨论】:

以上是关于当我只想在挂载时运行一次时,useEffect 缺少依赖项的主要内容,如果未能解决你的问题,请参考以下文章

停止在挂载上运行 useEffect

让useEffect监视条件并使其仅触发一次

当我只想在 JPG 和 PNG 之间转换时,为啥 ImageMagick 会改变图像亮度?

使用 useEffect 刷新表

React useEffect 调用 API 的时间太多,当我的组件渲染时,如何将 API 调用限制为一次?

排除这个 useEffect 依赖数组变量是不是明智? lint 推荐 3,但我只想依赖 1