Можете ли вы рано вернуться с крючками React?

Документы React дают понять, что условный вызов хуков не будет работать. Исходная презентация хуков React была вызвана тем, что React использует порядок, который вы называете хуками, для введения правильного значения.

Я понимаю это, но теперь мой вопрос заключается в том, можно ли рано возвращаться из компонента функции с крючками.

Так что-то вроде этого разрешено:

import React from 'react';
import { useRouter, Redirect } from 'react-router';
import { useSelector } from 'react-redux';

export default function Component() {
  const { match } = useRouter({ path: '/:some/:thing' });
  if (!match) return <Redirect to="/" />;

  const { some, thing } = match.params;
  const state = useSelector(stateSelector(some, thing));


  return <Blah {...state} />;
}

Технически, ловушка useSelector вызывается условно, однако порядок их вызова не меняется в зависимости от рендеринга (даже если возможно, что будет вызываться один меньший крючок).

Если это не разрешено, можете ли вы объяснить, почему это не разрешено, и предоставить общие альтернативные подходы к раннему возврату в компоненте функции с хуками?

Ответы

Ответ 1

React не позволяет вам сделать ранний возврат до других хуков. Если компонент выполняет меньше хуков, чем предыдущий рендер, вы получите следующую ошибку:

Нарушение инварианта: вынесено меньше хуков, чем ожидалось. Это может быть вызвано случайным выражением о досрочном возврате.

React не может определить разницу между ранним возвратом и условным вызовом ловушки. Например, если у вас есть 3 вызова на useState, и вы иногда возвращаетесь после второго, React не может определить, вернулись ли вы после второго вызова useState или если вы поставили условие вокруг первого или второго useState call, поэтому он не может достоверно знать, возвращает ли он правильное состояние для двух вызовов useState, которые выполняли.

Вот пример, который вы можете использовать, чтобы увидеть эту ошибку в действии (дважды нажмите кнопку "Increment State 1", чтобы получить ошибку):

import React from "react";
import ReactDOM from "react-dom";

function App() {
  const [state1, setState1] = React.useState(1);
  if (state1 === 3) {
    return <div>State 1 is 3</div>;
  }
  const [state2, setState2] = React.useState(2);
  return (
    <div className="App">
      <div>State 1: {state1}</div>
      <div>State 2: {state2}</div>
      <button onClick={() => setState1(state1 + 1)}>Increment State 1</button>
      <button onClick={() => setState2(state2 + 1)}>Increment State 2</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Edit Early return with hooks

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

В моем примере это может выглядеть следующим образом:

import React from "react";
import ReactDOM from "react-dom";

const AfterEarlyReturn = ({ state1, setState1 }) => {
  const [state2, setState2] = React.useState(2);
  return (
    <div className="App">
      <div>State 1: {state1}</div>
      <div>State 2: {state2}</div>
      <button onClick={() => setState1(state1 + 1)}>Increment State 1</button>
      <button onClick={() => setState2(state2 + 1)}>Increment State 2</button>
    </div>
  );
};
function App() {
  const [state1, setState1] = React.useState(1);
  if (state1 === 3) {
    return <div>State 1 is 3</div>;
  }
  return <AfterEarlyReturn state1={state1} setState1={setState1} />;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Edit Early return with hooks

Ответ 2

Это немного нюансированная проблема; вы можете досрочно вернуться из ловушки, но она должна быть последней в вашем компоненте. Хотя на самом деле это не кажется полезным шаблоном для возврата из ловушки - единственный раз, когда я мог это увидеть, это если компонент не является динамическим и основан на каком-то условии приложения, возвращающемся рано из своей ловушки. Но в этом случае все экземпляры должны быть одинаковыми для всех его времени выполнения - в противном случае правила хуков и их работа приведут к тому, что они не будут работать.

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

Итак, TL;DR выше: возвращение хука досрочно - это правильный вариант использования, но не очень полезный, потому что он всегда должен возвращаться рано.

Альтернативой является предварительное вычисление содержимого рендеринга для этого хука, а затем над нормальным оператором возврата вашего компонента, проверьте результат этого хука, чтобы увидеть, было ли у него возвращаемое содержимое. Важное соображение к этому подходу (поскольку я предполагаю, что вы хотели бы оптимизировать рендеринг здесь), это то, что все другие ловушки и логика впоследствии все равно будут выполняться.

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

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

Редактировать: re: downvote. Если есть причина, почему то, что я говорю, неверно, честно говоря, ради любви научиться знать, почему именно? (Комментарии тоже хорошо). Я проверил свою гипотезу, прежде чем писать это, и у меня не было проблем. Благодарю.

Ответ 3

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

import React from 'react';
import { useRouter, Redirect } from 'react-router';
import { useRedux } from 'react-redux';

const stateSelector =(match) => {
    if (!match) {
      return null;
    }
    const { some, thing } = match.params;
    // do what you want and return from here;
}
export default function Component() {
  const { match } = useRouter({ path: '/:some/:thing' });
  const state = useRedux(stateSelector(match));

  if (!match) return <Redirect to="/" />;
  return <Blah {...state} />;
}