根据恒定条件调用反应挂钩是不是安全?

Posted

技术标签:

【中文标题】根据恒定条件调用反应挂钩是不是安全?【英文标题】:Is it safe to call react hooks based on a constant condition?根据恒定条件调用反应挂钩是否安全? 【发布时间】:2019-08-24 20:49:03 【问题描述】:

Rules of Hooks 要求在每次渲染时都以相同的顺序调用相同的钩子。如果您违反此规则,则会出现错误的解释。例如这段代码:

function App() 
  console.log('render');
  const [flag, setFlag] = useState(true);
  const [first] = useState('first');
  console.log('first is', first);
  if (flag) 
    const [second] = useState('second');
    console.log('second is', second);
  
  const [third] = useState('third');
  console.log('third is', third);

  useEffect(() => setFlag(false), []);

  return null;

输出到控制台

render 
first is first 
second is second 
third is third 
render 
first is first 
third is second 

并导致警告或错误。

但是在元素生命周期内不改变的条件呢?

const DEBUG = true;

function TestConst() 
  if (DEBUG) 
    useEffect(() => console.log('rendered'));
  

  return <span>test</span>;

这段代码并没有真正违反规则,而且似乎工作正常。但它仍然会触发 eslint 警告。

此外,基于props编写类似的代码似乎是可能的:

function TestState(id, debug) 
  const [isDebug] = useState(debug);

  if (isDebug) 
    useEffect(() => console.log('rendered', id));
  

  return <span>id</span>;


function App() 
  const [counter, setCounter] = useState(0);
  useEffect(() => setCounter(1), []);
  return (
    <div>
      <TestState id="1" debug=false/>
      <TestState id="2" debug=true/>
    </div>
  );

此代码按预期工作。

那么当我确定它不会改变时,在条件中调用钩子是否安全?是否可以修改 eslint 规则来识别这种情况?

问题更多是关于真正的需求,而不是实现类似行为的方式。据我了解,这很重要

确保每次组件都以相同的顺序调用 Hook 呈现。这就是允许 React 正确保存 多个 useState 和 useEffect 调用之间的挂钩

这条规则有一个例外:“不要在循环、条件或嵌套函数中调用 Hooks”。

【问题讨论】:

【参考方案1】:

虽然您可以像上面提到的那样有条件地编写钩子并且它可能在当前工作,但它可能会导致将来出现意外行为。例如,在当前情况下,您没有修改 isDebug 状态。

演示

const useState, useEffect = React;
function TestState(id, debug) 
  const [isDebug, setDebug] = useState(debug);

  if (isDebug) 
    useEffect(() => console.log('rendered', id));
  
  
  const toggleButton = () => 
    setDebug(prev => !prev);
  

  return (
    <div>
      <span>id</span>
       <button type="button" onClick=toggleButton>Toggle debug</button>
    </div>
  );


function App() 
  const [counter, setCounter] = useState(0);
  useEffect(() => setCounter(1), []);
  return (
    <div>
      <TestState id="1" debug=false/>
      <TestState id="2" debug=true/>
    </div>
  );


ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app"/>

根据经验,您不应该违反规则,因为这可能会在将来引起问题。您可以通过以下方式处理上述情况而不违反规则

const useState, useEffect = React;
function TestState(id, debug) 
  const [isDebug, setDebug] = useState(debug);

    useEffect(() => 
      if(isDebug) 
        console.log('rendered', id)
      
    , [isDebug]);
  
  const toggleButton = () => 
    setDebug(prev => !prev);
  

  return (
    <div>
      <span>id</span>
       <button type="button" onClick=toggleButton>Toggle debug</button>
    </div>
  );


function App() 
  const [counter, setCounter] = useState(0);
  useEffect(() => setCounter(1), []);
  return (
    <div>
      <TestState id="1" debug=false/>
      <TestState id="2" debug=true/>
    </div>
  );


ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app"/>

【讨论】:

谢谢。我了解TestState 依赖于当前的“每个元素”要求,如果某些内部反应更改将其变为“每个组件”要求,则可能会中断。但我无法想象const DEBUG 将来会如何崩溃。 const [isDebug] = useState(debug); 也是这样写的,以确保它不会改变。 @UjinT34,目前你并没有修改isDebug状态,但未来可能有人只是添加一个setter函数并提供一个修改isDebug状态的方法。这可能会破坏应用程序。 一个问题是,一旦您想要有条件地使用的钩子是例如包含大量 use* 调用的自定义钩子,这种方法会导致 大量 样板代码.【参考方案2】:

对于您的用例,我看不到问题所在,我看不出这在未来会如何中断,您说得对,它按预期工作。

但是,我认为警告实际上是合法的,并且应该始终存在,因为这可能是您代码中的潜在错误(不是在这个特定的代码中)

所以在你的情况下,我会为该行禁用 react-hooks/rules-of-hooks 规则。

参考:https://reactjs.org/docs/hooks-rules.html

【讨论】:

【参考方案3】:

这个钩子规则解决了条件钩子调用可能出现问题的常见情况:

不要在循环、条件或嵌套函数中调用 Hook。相反,请始终在 React 函数的顶层使用 Hooks。通过遵循此规则,您可以确保每次组件呈现时都以相同的顺序调用 Hook。

如果开发人员没有完全意识到后果,则此规则是一个安全的选择,可以用作经验法则​​。

但这里的实际规则是:

确保每次渲染组件时都以相同的顺序调用 Hooks

使用循环、条件和嵌套函数完全没问题,只要保证在同一组件实例中以相同的数量和顺序调用钩子

如果在运行时重新分配 process.env.NODE_ENV 属性,即使 process.env.NODE_ENV === 'development' 条件也可能在组件生命周期内发生变化。

如果条件是常量,则可以在组件外部定义它以保证:

const isDebug = process.env.NODE_ENV === 'development';

function TestConst() 
  if (isDebug) 
    useEffect(...);
  
  ...

如果一个条件来自动态值(特别是初始道具值),它可以被记忆:

function TestConst( debug ) 
  const isDebug = useMemo(() => debug, []);

  if (isDebug) 
    useEffect(...);
  
  ...

或者,因为 useMemo isn't guaranteed to preserve values 在未来的 React 版本中,useState(如问题所示)或 useRef 可以使用;后者没有额外的开销和合适的语义:

function TestConst( debug ) 
  const isDebug = useRef(debug).current;

  if (isDebug) 
    useEffect(...);
  
  ...

如果有react-hooks/rules-of-hooks ESLint 规则,可以按行禁用。

【讨论】:

【参考方案4】:

请不要使用这种模式。它可能在您的示例中有效,但它不是很好(或惯用的)。

标准模式(有充分的理由)是在构造函数中声明初始状态,然后根据主体中的某些条件(setState)进行更新。 React Hooks 在无状态组件中反映了这个功能——所以它应该是一样的。

其次,我看不出动态添加这种状态有什么用处,并可能导致稍后出现渲染问题。在您的示例中,一个简单的 const 也可以正常工作 - 没有理由使用动态状态。

考虑一下:

return (<React.Fragment>second</React.Fragment>)

只要您没有定义second,就会出现引用错误。

【讨论】:

以上是关于根据恒定条件调用反应挂钩是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

无效的挂钩调用。钩子只能在反应函数组件内部使用...... useEffect,redux

收到与反应挂钩 useHistory 相关的错误

Spring Cloud中如何保证各个微服务之间调用的安全性

React Hooks:在验证时实例化状态挂钩错误:无效的挂钩调用。 Hooks 只能在函数组件的主体内部调用

如何访问在另一个 js 文件中的反应挂钩中完成的状态值

反应上下文挂钩更新后如何触发功能