最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Is it safe to call react hooks based on a constant condition? - Stack Overflow

programmeradmin2浏览0评论

The Rules of Hooks require that the same hooks and in the same order are called on every render. And there is an explanation about what goes wrong if you break this rule. For example this code:

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;
}

Outputs to console

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

And causes a warning or an error.

But what about conditions that do not change during the element lifecycle?

const DEBUG = true;

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

  return <span>test</span>;
}

This code doesn't really break the rules and seems to work fine. But it still triggers the eslint warning.

Moreover it seems to be possible to write similar code based on 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>
  );
}

This code works as intended.

So is it safe to call hooks inside a condition when I am sure that it is not going to change? Is it possible to modify the eslint rule to recognise such situations?

The question is more about the real requirement and not the way to implement similar behaviour. As far as I understand it is important to

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

And there is a place for exceptions to this rule: "Don’t call Hooks inside loops, conditions, or nested functions".

The Rules of Hooks require that the same hooks and in the same order are called on every render. And there is an explanation about what goes wrong if you break this rule. For example this code:

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;
}

Outputs to console

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

And causes a warning or an error.

But what about conditions that do not change during the element lifecycle?

const DEBUG = true;

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

  return <span>test</span>;
}

This code doesn't really break the rules and seems to work fine. But it still triggers the eslint warning.

Moreover it seems to be possible to write similar code based on 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>
  );
}

This code works as intended.

So is it safe to call hooks inside a condition when I am sure that it is not going to change? Is it possible to modify the eslint rule to recognise such situations?

The question is more about the real requirement and not the way to implement similar behaviour. As far as I understand it is important to

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

And there is a place for exceptions to this rule: "Don’t call Hooks inside loops, conditions, or nested functions".

Share Improve this question edited Apr 3, 2019 at 11:26 UjinT34 asked Apr 3, 2019 at 10:41 UjinT34UjinT34 4,9871 gold badge13 silver badges27 bronze badges
Add a comment  | 

4 Answers 4

Reset to default 6 +50

Although you can write hooks conditionally like you mentioned above and it may work currently, it can lead to unexpected behavior in the future. For instance in the current case you aren't modifying the isDebug state.

Demo

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"/>

As a rule of thumb you shouldn't violate the rules since it may cause problems in future. You could handle the above scenarios in the following way without violating the rules

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"/>

For your use-case I don't see the problem, I don't see how this can break in the future, and you are right that it works as intended.

However, I think the warning is actually legit and should be there at all times, because this can be a potential bug in your code (not in this particular one)

So what I'd do in your case, is to disable react-hooks/rules-of-hooks rule for that line.

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

This hook rule address common cases when problems that may occur with conditional hook calls:

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.

If a developer isn't fully aware of consequences, this rule is a safe choice and can be used as a rule of thumb.

But the actual rule here is:

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

It's perfectly fine to use loops, conditions and nested functions, as long as it's guaranteed that hooks are called in the same quantity and order within the same component instance.

Even process.env.NODE_ENV === 'development' condition can change during component lifespan if process.env.NODE_ENV property is reassigned at runtime.

If a condition is constant, it can be defined outside a component to guarantee that:

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

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

In case a condition derives from dynamic value (notably initial prop value), it can be memoized:

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

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

Or, since useMemo isn't guaranteed to preserve values in future React releases, useState (as the question shows) or useRef can be used; the latter has no extra overhead and a suitable semantics:

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

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

In case there's react-hooks/rules-of-hooks ESLint rule, it can be disabled per line basis.

Please don't use this pattern. It may work in your example but it is not nice (or idiomatic).

The standard pattern (for good reason) is that initial state is declared in the constructor and then updated in response to some condition in the body (setState). React Hooks mirror this functionality in stateless components - so it should work the same.

Secondly, I cannot see how it is useful to dynamically add this piece of state and potentially cause rendering problems later down the line. In your example, a simple const would work just as well - there is no reason to use dynamic state.

Consider this:

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

This breaks with a Reference error whenever you don't have second defined.

发布评论

评论列表(0)

  1. 暂无评论