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

时间:2019-12-26 17:04:49

标签: reactjs react-hooks

根据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>
  );
}

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

ESLint警告

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

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

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

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

但是在乌龟方面,ESLint抱怨说useCallback效果缺少name依赖性。添加该依赖关系会破坏预期的语义,因为该警报现在会在 name 状态更新时随时触发。

解决方案?

这是一个简单的,有些人为的示例,但是它经常出现在我一直在研究的多个代码库中。场景很简单。您在组件内部使用函数有一些状态。该函数在组件内的多个位置(在useEffect挂钩的内部和外部)都被调用。您希望useEffect挂钩仅在单个道具或状态发生变化时才调用该函数。

React的文档建议最好的解决方案是将功能移到useEffect挂钩内。但这将阻止它在组件中的其他位置使用。下一个建议是将功能包括在依赖列表中,并在需要时使用useCallback()挂钩将其包装。但是,在许多情况下,这可能会引入不受欢迎的行为,或者只是将ESLint错误引导至useCallback()。

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

1 个答案:

答案 0 :(得分:0)

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

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

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

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

是-尽管有点难看,但是使用useReducer可以实现所需的语义。下面的代码很丑陋,因为我们正在使用化简器来触发函数,而不是按预期使用它来更新状态。

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 状态时触发,而警报仍将捕获并显示对的任何更改>名称状态。