为啥从 React 的 useEffect 依赖列表中省略函数是不安全的?

Posted

技术标签:

【中文标题】为啥从 React 的 useEffect 依赖列表中省略函数是不安全的?【英文标题】:Why is omitting functions from React's useEffect dependency list unsafe?为什么从 React 的 useEffect 依赖列表中省略函数是不安全的? 【发布时间】:2020-04-16 20:50:11 【问题描述】:

根据React Hook FAQ,

安全地从依赖项列表中省略一个函数(如果没有) 其中(或由它调用的函数)引用道具、状态或 从它们派生的值。

FAQ 然后继续给出一个示例,其中该函数被省略,并说代码包含错误。但是,常见问题解答从未提及错误是什么。

我做了一个类似的例子,我创建了一个使用两个状态的函数。然后从 useEffect 挂钩调用该函数,该挂钩在其依赖项列表中只有一个状态。然而,即使有关于缺少依赖项的承诺 ESLint 警告,该函数和 useEffect 挂钩仍按预期工作。

Code sandbox of the example

预期语义:

点击问候按钮时显示警报(直接函数调用) “问候”状态更改时显示警报(通过 useEffect) 名称更改时不显示警告。 每当显示问候语时,都会使用最后指定的名称。

代码:

export function UseEffectEx(props) 
  const [greeting, setGreeting] = useState("Hello");
  const [name, setName] = useState("John");
  const [randomNumber, setRandomNumber] = useState(Math.random());

  function greet() 
    alert(`$greeting, $name.`);
  

  useEffect(
    function greetOnGreetingChange() 
      greet();
    ,
    [greeting]
  );

  return (
    <div>
      <button onClick=greet>Greet</button>
      <button onClick=() => setGreeting("Hello")>
        set greeting to 'Hello'
      </button>
      <button onClick=() => setGreeting("Goodbye")>
        set greeting to 'Goodbye'
      </button>
      <button onClick=() => setName("John")>set name to 'John'</button>
      <button onClick=() => setName("Jane")>set name to 'Jane'</button>
      <button onClick=() => setRandomNumber(Math.random())>
        generate random
      </button>
      <p>Random number = $randomNumber</p>
    </div>
  );


所有预期的语义都得到满足。 Curcily,使用按钮更改名称状态不会触发警报,但触发警报时始终使用正确的名称。

ESLint 警告

上面的代码在 useEffect() 的依赖列表上产生了承诺的 react-hooks/exhaustive-deps 警告。警告说钩子缺少greet() 的依赖项。警告的自动修复是添加 greet 作为依赖项。

  useEffect(
    function greetOnGreetingChange() 
      greet();
    ,
    [greeting, greet]
  );

但是,这会产生另一个 ESLint 错误,这次是在 greet() 函数上。该错误表明该函数在每次渲染时都被调用。单击 generate random 按钮可确认此意外行为。 ESLint 建议将greet() 函数包裹在useCallback 效果中,例如:

  const greet = useCallback(function greet() 
    alert(`$greeting, $name.`)
  , [greeting]);

但是在海龟的情况下,ESLint 抱怨 useCallback 效果缺少 name 依赖项。添加该依赖项会破坏预期的语义,因为现在警报将在 name 状态更新时触发。

解决方案?

这是一个简单的、有点做作的示例,但它经常出现在我一直在处理的多个代码库中。场景很简单。您在组件内使用函数有一些状态。该函数在组件内的多个位置被调用,包括在 useEffect 钩子内部和外部。您希望 useEffect 挂钩仅在单个 prop 或状态发生更改时调用该函数。

React 的文档建议最好的解决方案是将函数移动到 useEffect 挂钩中。但这会阻止它在组件内的其他地方使用。下一个建议是将函数包含在依赖列表中,并在需要时用 u​​seCallback() 钩子包装它。然而,在许多情况下,这要么引入了不受欢迎的行为,要么只是将 ESLint 错误引导到 useCallback()。

React 想要防范的原始代码中的“bug”是什么?除了禁用 ESLint 检查之外,还有其他解决方案吗?

【问题讨论】:

来自 Dan Abramov 本人:overreacted.io/a-complete-guide-to-useeffect 【参考方案1】:

基于Dan Abramov Use Effect article(@Bennett Dams 提供),没有很好的解决方案。

问题:代码中的错误是什么?

ESList 警告要防范的错误是名称状态更改时不会触发效果。然而,由于这种行为是有意为之,更大的问题是代码的语义取决于状态变化,有时有时会导致更新,有时则不会。这似乎与 React Hooks 的潮流背道而驰。

 

问题:好的,但我还是想做。除了禁用 ESLint 警告还有其他解决方案吗?

是的 - 虽然有点难看,但使用 useReducer 可以实现所需的语义。下面的代码很难看,因为我们使用 reducer 来触发一个函数,而不是按预期使用它,即更新状态。

function Example(props) 
  const [greeting, setGreeting] = useState('Hello');
  const [name, setName] = useState('John');
  const [randomNumber, setRandomNumber] = useState(Math.random());
  const [_, dispatch] = useReducer(reducer, );

  function greet() 
    alert(`$greeting, $name.`)
  

  function reducer(state, action) 
    if (action.type === 'alert') 
      greet();
    
    return state;
  


  useEffect(function greetOnGreetingChange() 
    dispatch(type: 'alert')
  , [dispatch, greeting]);

  return (
    <div>
      <button onClick=() => setGreeting('Hello')>set greeting to 'Hello'</button>
      <button onClick=() => setGreeting('Goodbye')>set greeting to 'Goodbye'</button>
      <button onClick=() => setName('John')>set name to 'John'</button>
      <button onClick=() => setName('Jane')>set name to 'Jane'</button>
      <button onClick=() => setRandomNumber(Math.random()) >generate random</button>
      <button onClick=() => greet()>greet</button>
      <p>Random number = $randomNumber</p>

    </div>
  )

由于dispatch 保证在渲染中始终保持一致,因此效果只会在 greeting 状态更改时触发,而警报仍会捕获并显示对 name 的任何更改 状态。

【讨论】:

以上是关于为啥从 React 的 useEffect 依赖列表中省略函数是不安全的?的主要内容,如果未能解决你的问题,请参考以下文章

React 为啥将函数包含在依赖项列表中被认为是一种性能优化?

React useEffect 清理函数中的依赖没有更新

React useEffect() 无限重新渲染以获取所有尽管有依赖关系

基本反应问题。为啥在 React 文档的这个例子中需要 useEffect ?

UseEffect - React Hook useEffect 缺少依赖项:

如果添加了功能依赖关系,则 React useEffect re Renders Infinitely