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

时间:2019-04-03 10:41:32

标签: javascript reactjs eslint react-hooks

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警告。

此外,似乎有可能基于道具编写类似的代码:

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”。

4 个答案:

答案 0 :(得分:7)

尽管您可以像上面提到的那样有条件地编写钩子,并且它可能在当前工作,但是将来可能导致预期的行为。例如,在当前情况下,您没有修改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"/>

答案 1 :(得分:3)

对于您的用例,我看不到问题,也看不到将来如何解决,您是正确的,它按预期工作。

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

因此,根据您的情况,我将为该行禁用react-hooks/rules-of-hooks规则。

ref:https://reactjs.org/docs/hooks-rules.html

答案 2 :(得分:1)

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

  

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

如果开发人员不完全了解后果,那么此规则是安全的选择,可以用作经验法则​​。

但是这里的实际规则是:

  

确保每次渲染组件时,挂钩的调用顺序都相同

使用循环,条件和嵌套函数是完全可以的,只要保证在相同组件实例中以相同数量和顺序在相同的数量和顺序中调用钩子即可。

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

如果条件为常数,则可以在组件外部定义该条件以保证:

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

答案 3 :(得分:0)

请不要使用此模式。在您的示例中可能会起作用,但效果不好(或惯用的)。

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

第二,我看不到动态添加此状态并在以后产生渲染问题的可能性。在您的示例中,简单的const也可以工作-没有理由使用动态状态。

考虑一下:

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

每当您未定义second时,就会出现参考错误。