Безопасно ли вызывать ответные хуки, основанные на постоянном условии? - PullRequest
7 голосов
/ 03 апреля 2019

Правила хуков требуют, чтобы при каждом рендере вызывались одинаковые хуки и в одинаковом порядке.И есть объяснение того, что пойдет не так, если вы нарушите это правило.Например, этот код:

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 для распознавания таких ситуаций?

Вопрос скорее в реальных требованиях, а не в способе реализации подобного поведения.Насколько я понимаю, важно

гарантировать, что хуки вызываются в одном и том же порядке каждый раз при рендеринге компонента.Это то, что позволяет React правильно сохранять состояние хуков между несколькими вызовами useState и useEffect

И есть место для исключений из этого правила: «Не вызывайте хуки внутри циклов, условий или вложенных элементов».функции».

Ответы [ 4 ]

7 голосов
/ 03 апреля 2019

Хотя вы можете написать условные зацепки, как вы упомянули выше, и это может работать в настоящее время, но это может привести к ожидаемому поведению в будущем.Например, в текущем случае вы не изменяете состояние 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"/>
4 голосов
/ 05 апреля 2019

Для вашего случая использования я не вижу проблемы, я не вижу, как это может сломаться в будущем, и вы правы, что это работает как задумано.

Однако я думаю, чтопредупреждение действительно допустимо и должно быть всегда, потому что это может быть потенциальной ошибкой в ​​вашем коде (не в этом конкретном)

Так что я бы сделал в вашем случае, чтобы отключить react-hooks/rules-of-hooks правило для этой строки.

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

1 голос
/ 07 апреля 2019

Это правило перехвата обращается к распространенным случаям, когда проблемы, которые могут возникнуть с условными вызовами перехвата:

Не вызывать перехватчики внутри циклов, условий или вложенных функций.Вместо этого всегда используйте Hooks на верхнем уровне вашей функции React.Следуя этому правилу, вы гарантируете, что хуки вызываются в одном и том же порядке каждый раз при визуализации компонента.

Если разработчик не полностью осознает последствия, это правило является безопасным выбором и может бытьиспользуется как практическое правило.

Но фактическое правило здесь таково:

гарантирует, что хуки вызываются в одном и том же порядке каждый раз, когда компонент отображает

Прекрасно использовать циклы, условия и вложенные функции, если гарантируется, что хуки вызываются в одинакового количества и порядка в одном экземпляре компонента .

Четный process.env.NODE_ENV === 'development' условие может измениться в течение срока службы компонента, если свойство process.env.NODE_ENV переназначено во время выполнения.

Если условие является постоянным, его можно определить вне компонента, чтобы гарантировать:

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

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

InЕсли условие происходит от динамического значения (в частности, начального значения пропа), его можно запомнить:

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

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

Или, поскольку useMemo не гарантирует сохранение значений в будущих выпусках React можно использовать useState (как показывает вопрос) или useRef;последний не имеет никаких дополнительных издержек и подходящей семантики:

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

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

В случае react-hooks/rules-of-hooks правила ESLint его можно отключить для каждой строки.

0 голосов
/ 05 апреля 2019

Пожалуйста, не используйте этот шаблон. Это может работать в вашем примере, но это не красиво (или идиоматично).

Стандартный шаблон (по уважительной причине) заключается в том, что начальное состояние объявляется в конструкторе, а затем обновляется в ответ на некоторое условие в теле (setState). Крюки React отражают эту функциональность в компонентах без сохранения состояния, поэтому они должны работать одинаково.

Во-вторых, я не могу понять, как полезно динамически добавлять этот фрагмент состояния и потенциально вызывать проблемы рендеринга в дальнейшем. В вашем примере простой const будет работать так же хорошо - нет причин использовать динамическое состояние.

Учтите это:

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

Это приводит к ошибке ссылки, если вы не определили second.

...