基于恒定条件调用反应挂钩是否安全?

Is it safe to call react hooks based on a constant condition?

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 规则以识别此类情况?

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

ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls

这条规则也有例外:"Don’t call Hooks inside loops, conditions, or nested functions"。

虽然您可以像上面提到的那样有条件地编写挂钩并且它目前可能有效,但它可能会导致将来出现意外行为。例如,在当前情况下,您没有修改 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"/>

对于您的用例,我没有看到问题,我没有看到这在未来会如何中断,您是对的,它按预期工作。

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

那么对于你的情况,我会做的是禁用该行的 react-hooks/rules-of-hooks 规则。

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

请不要使用此模式。它可能适用于您的示例,但它并不好(或惯用)。

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

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

考虑一下:

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

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

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

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders.

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

但这里的实际规则是:

ensure that Hooks are called in the same order each time a component renders

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

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

如果条件不变,则可以在组件外部定义它以保证:

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

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

如果条件源自动态值(特别是初始 prop 值),它可以被记忆:

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 规则,可以按行禁用它。