【问题标题】:Order of hooks error when rendering different components渲染不同组件时的钩子顺序错误
【发布时间】:2021-10-26 11:05:21
【问题描述】:

当我尝试渲染不同的组件时,React 会抛出以下错误

警告:React 检测到 GenericDialog 调用的 Hook 的顺序发生了变化。如果不修复,这将导致错误和错误。

Previous render Next render
useRef useRef
useState useState
useState useState
useState useState
useState useState
useState useState
useContext useState

我同意这在我每次渲染相同的组件但使用不同的钩子顺序时是不合适的。我想要实现的是每次渲染一个不同的组件,所以很明显钩子的顺序不会相同。

我创建了这个GenericDialog 组件,它呈现一个多步对话框。

import React, { useRef, useState, useEffect } from 'react';
import { DialogFooterNavigation } from './DialogFooterNavigation';
import { Dialog } from '../../../../Dialog';
import { Subheader } from '../../../../Subheader';
import { Loading } from '../../../../Loading';

export interface FooterConfiguration {
  onContinue?: () => Promise<boolean | void>;
  isContinueDisabled?: boolean;
  continueLabel?: string;
  isBackHidden?: boolean;
  isCancelHidden?: boolean;
}

export interface HeaderConfiguration {
  subheader?: string;
}
export interface DialogStepProps {
  setHeaderConfiguration: (config: HeaderConfiguration) => void;
  setFooterConfiguration: (config: FooterConfiguration) => void;
}

export type DialogStep = (props: DialogStepProps) => JSX.Element;

interface GenericDialogProps {
  isShown: boolean;
  hideDialog: () => void;
  steps: DialogStep[];
  header: string;
}

export const GenericDialog = ({
  isShown,
  hideDialog,
  steps,
  header,
}: GenericDialogProps) => {
  const buttonRef = useRef(null);
  const [step, setStep] = useState<number>(0);
  const [isLoading, setIsLoading] = useState<boolean>(false);

  const [headerConfiguration, setHeaderConfiguration] = useState<HeaderConfiguration | undefined>(
    undefined,
  );
  const [footerConfiguration, setFooterConfiguration] = useState<FooterConfiguration | undefined>(
    undefined,
  );
  const [loadingMessage, setLoadingMessage] = useState<string>('');

  const dialogBody = steps[step]({
    setHeaderConfiguration,
    setFooterConfiguration,
  });

  const nextStep = () => {
    if (step < steps.length - 1) {
      setStep(step + 1);
    }
  };

  const prevStep = () => step > 0 && setStep(step -1);

  const isBackPossible = step > 0;
  const onBack = () => (isBackPossible || footerConfiguration?.isBackHidden ? undefined : prevStep);

  const onContinue = async () => {
    setIsLoading(true);

    const result = await footerConfiguration?.onContinue?.call(undefined);
    setIsLoading(false);

    if (result === false) {
      return;
    }

    nextStep();
  };

  return (
    <Dialog isShown={isShown} onHide={hideDialog}>
      <div>
        {header}
        {headerConfiguration?.subheader && (
          <Subheader>{headerConfiguration.subheader}</Subheader>
        )}
      </div>

      {isLoading && loadingMessage ? <Loading msg={loadingMessage} /> : dialogBody}

      {!isLoading && (
        <DialogFooterNavigation
          onBack={isBackPossible ? onBack : undefined}
          onContinue={onContinue}
          isContinueDisabled={footerConfiguration?.isContinueDisabled}
        />
      )}
    </Dialog>
  );
};

const FirstStep = (props: DialogStepProps) => {
  // Here I need useContext
  const { id, name } = useCustomerContext();
  
  useEffect(() => {
    props.setFooterConfiguration({
      isContinueDisabled: !id || !name,
    })
  }, [id, name]);
  
  return (
    <>
      <div>ID: {id}</div>
      <div>Name: {name}</div>
    </>
  );
};

const SecondStep = (props: DialogStepProps) => {
  // Here I don't need useContext but I do need useState
  const [inputValue, setInputValue] = useState({});
  
  useEffect(() => {
    props.setFooterConfiguration({
      isContinueDisabled: !inputValue,
    });
  }, [inputValue]);
  
  return <input value={inputValue} onChange={(event) => setInputValue(event.target.value)} />;
}


const MyDialogExample = () => {
  const [isDialogOpen, setIsDialogOpen] = useState(false);
  
  const steps: DialogStep[] = [
    FirstStep,
    SecondStep,
  ];
  
  return (
    <>
      <button onClick={() => setIsDialogOpen(true)}>Open Dialog</button>
      
      <GenericDialog
        isShown={isDialogOpen}
        hideDialog={() => setIsDialogOpen(false)}
        steps={steps}
        header="Dialog example"
      />
    </>
  );
};

【问题讨论】:

    标签: javascript reactjs typescript


    【解决方案1】:

    问题出在这里:

    const dialogBody = steps[step]({
      setHeaderConfiguration,
      setFooterConfiguration,
    });
    

    试着把它改成这样:

    const DialogBody = steps[step];
    

    然后,在您的退货声明中:

    {isLoading && loadingMessage ? <Loading msg={loadingMessage} /> : <DialogBody setHeaderConfiguration={setHeaderConfiguration} setFooterConfiguration={setFooterConfiguration} />}
    

    请注意,它可以以不同的方式完成,例如:

    const DialogBody = steps[step];
    
    const dialogBody = <DialogBody setHeaderConfiguration={setHeaderConfiguration} setFooterConfiguration={setFooterConfiguration} />;
    

    并保持您的退货声明不变。

    说明

    不过,您的代码并非完全错误。在使用函数式组件时,实际组件、钩子和基于某些逻辑返回实例化组件的简单函数之间存在细微差别。问题是你把这三个混在一起了。

    你不能通过调用相应的函数来手动实例化一个组件(就像你不能用new操作符来实例化一个类组件一样)。要么使用 JSX(如 &lt;DialogBody /&gt;),要么直接使用 React 内部方法(如 React.createElement())。这两种选择都不同于仅仅做dialogBody()。例如,如果您看到编译后的 JSX 代码,您会注意到 &lt;DialogBody /&gt; 编译为使用 React.createElement() 的代码,后者返回一个包含许多特殊属性和方法的真实 React 元素实例。

    如果

    dialogBody() 的唯一目标是基于某种逻辑返回一个实例化的元素(使用上述方法之一),它将起作用。这意味着不使用任何钩子以及其他一些约束。

    相反,您的dialogBody“函数”包含挂钩,它本身充当自定义挂钩。这就是 React 抱怨 hooks 执行顺序的原因。您正在有条件地执行钩子。

    【讨论】:

    • 是的,谢谢您的解释!这正是问题所在。
    • 你知道如何解决这个警告吗?我相信一旦我在组件中创建了这个组件,它就会出现。警告:React 无法识别 DOM 元素上的 setDialogConfiguration 属性。如果您有意希望它作为自定义属性出现在 DOM 中,请将其拼写为小写 setdialogconfiguration。如果您不小心从父组件中传递了它,请将其从 DOM 元素中移除。
    • 这种方法还产生了另一个错误。每次重新渲染组件时都会重置步骤中的状态,就像重新创建而不是重新渲染一样。我在 FirstStep 中有一个验证状态,但每次输入更改时状态都会重置。
    • @LazioTibijczyk setDialogConfiguration 警告很容易解决,但我在您的代码中看不到该部分。之所以会发生这种情况,是因为某些自定义 prop 降落在本机 DOM 元素(如 &lt;div setDialogConfiguration={setDialogConfiguration}&gt;&lt;/div&gt;)上,可能是因为扩展运算符(如
      )。你必须像 const {setDialogConfiguration, ...other} = props; 这样解构 setDialogConfiguration,然后像 &lt;div {...other}&gt;&lt;/div&gt; 这样传播。
    • @LazioTibijczyk 第二个问题有点难以调试,但它肯定不是由“这种方法”引起的,而是由你原来的方法引起的。我看到您在 SecondStep 组件中执行 value={value} 。你的意思是value={inputValue}
    猜你喜欢
    • 2016-03-04
    • 2020-11-30
    • 2023-02-13
    • 2019-10-02
    • 1970-01-01
    • 2021-12-01
    • 2022-08-03
    • 2021-10-12
    • 2021-02-09
    相关资源
    最近更新 更多