未捕获的不变违规:重新渲染过多。React 限制渲染次数以防止无限循环

IT技术 javascript reactjs redux material-ui
2021-04-20 08:11:28

我正在尝试添加一个零食栏,以便在用户登录与否时显示一条消息。SnackBar.jsx:

import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import CheckCircleIcon from "@material-ui/icons/CheckCircle";
import ErrorIcon from "@material-ui/icons/Error";
import CloseIcon from "@material-ui/icons/Close";
import green from "@material-ui/core/colors/green";
import IconButton from "@material-ui/core/IconButton";
import Snackbar from "@material-ui/core/Snackbar";
import SnackbarContent from "@material-ui/core/SnackbarContent";
import { withStyles } from "@material-ui/core/styles";

const variantIcon = {
  success: CheckCircleIcon,
  error: ErrorIcon
};

const styles1 = theme => ({
  success: {
    backgroundColor: green[600]
  },
  error: {
    backgroundColor: theme.palette.error.dark
  },
  icon: {
    fontSize: 20
  },
  iconVariant: {
    opacity: 0.9,
    marginRight: theme.spacing.unit
  },
  message: {
    display: "flex",
    alignItems: "center"
  }
});

function SnackbarContentWrapper(props) {
  const { classes, className, message, onClose, variant, ...other } = props;
  const Icon = variantIcon[variant];

  return (
    <SnackbarContent
      className={classNames(classes[variant], className)}
      aria-describedby="client-snackbar"
      message={(
        <span className={classes.message}>
          <Icon className={classNames(classes.icon, classes.iconVariant)} />
          {message}
        </span>
      )}
      action={[
        <IconButton
          key="close"
          aria-label="Close"
          color="inherit"
          className={classes.close}
          onClick={onClose}
        >
          <CloseIcon className={classes.icon} />
        </IconButton>
      ]}
      {...other}
    />
  );
}

SnackbarContentWrapper.propTypes = {
  classes: PropTypes.shape({
    success: PropTypes.string,
    error: PropTypes.string,
    icon: PropTypes.string,
    iconVariant: PropTypes.string,
    message: PropTypes.string,
  }).isRequired,
  className: PropTypes.string.isRequired,
  message: PropTypes.node.isRequired,
  onClose: PropTypes.func.isRequired,
  variant: PropTypes.oneOf(["success", "error"]).isRequired
};

const MySnackbarContentWrapper = withStyles(styles1)(SnackbarContentWrapper);

const CustomizedSnackbar = ({
  open,
  handleClose,
  variant,
  message
}) => {
  return (
    <div>
      <Snackbar
        anchorOrigin={{
          vertical: "bottom",
          horizontal: "left"
        }}
        open={open}
        autoHideDuration={6000}
        onClose={handleClose}
      >
        <MySnackbarContentWrapper
          onClose={handleClose}
          variant={variant}
          message={message}
        />
      </Snackbar>
    </div>
  );
};

CustomizedSnackbar.propTypes = {
  open: PropTypes.bool.isRequired,
  handleClose: PropTypes.func.isRequired,
  variant: PropTypes.string.isRequired,
  message: PropTypes.string.isRequired
};

export default CustomizedSnackbar;

SignInFormContainer.jsx:

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import SnackBar from '../../components/SnackBar';
import SignInForm from './SignInForm';

const SingInContainer = ({ message, variant}) => {
    const [open, setSnackBarState] = useState(false);
    const handleClose = (reason) => {
        if (reason === 'clickaway') {
          return;
        }
        setSnackBarState(false)

      };

    if (variant) {
        setSnackBarState(true);
    }
    return (
        <div>
        <SnackBar
            open={open}
            handleClose={handleClose}
            variant={variant}
            message={message}
            />
        <SignInForm/>
        </div>
    )
}

SingInContainer.propTypes = {
    variant: PropTypes.string.isRequired,
    message: PropTypes.string.isRequired
}

const mapStateToProps = (state) => {
    const {variant, message } = state.snackBar;

    return {
        variant,
        message
    }
}

export default connect(mapStateToProps)(SingInContainer);

当我运行应用程序时,我收到此错误:

Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.
at invariant (http://localhost:9000/bundle.js:34484:15)
at dispatchAction (http://localhost:9000/bundle.js:47879:44)
at SingInContainer (http://localhost:9000/bundle.js:79135:5)
at renderWithHooks (http://localhost:9000/bundle.js:47343:18)
at updateFunctionComponent (http://localhost:9000/bundle.js:49010:20)
at beginWork (http://localhost:9000/bundle.js:50020:16)
at performUnitOfWork (http://localhost:9000/bundle.js:53695:12)
at workLoop (http://localhost:9000/bundle.js:53735:24)
at HTMLUnknownElement.callCallback (http://localhost:9000/bundle.js:34578:14)
at Object.invokeGuardedCallbackDev (http://localhost:9000/bundle.js:34628:16)

问题是由 SnackBar 组件引起的。我使用useState钩子来改变snackBar 的状态。我应该使用 class 和 acomponentShouldUpdate以便不渲染多次吗?

6个回答

我怀疑问题在于您立即在函数组件体内调用状态设置器,这迫使 React 使用相同的props再次重新调用您的函数,最终再次调用状态设置器,从而触发react再次调用你的函数......等等。

const SingInContainer = ({ message, variant}) => {
    const [open, setSnackBarState] = useState(false);
    const handleClose = (reason) => {
        if (reason === 'clickaway') {
          return;
        }
        setSnackBarState(false)

      };

    if (variant) {
        setSnackBarState(true); // HERE BE DRAGONS
    }
    return (
        <div>
        <SnackBar
            open={open}
            handleClose={handleClose}
            variant={variant}
            message={message}
            />
        <SignInForm/>
        </div>
    )
}

相反,我建议您只使用三元有条件地设置 state 属性的默认值,这样您最终会得到:

const SingInContainer = ({ message, variant}) => {
    const [open, setSnackBarState] = useState(variant ? true : false); 
                                  // or useState(!!variant); 
                                  // or useState(Boolean(variant));
    const handleClose = (reason) => {
        if (reason === 'clickaway') {
          return;
        }
        setSnackBarState(false)

      };

    return (
        <div>
        <SnackBar
            open={open}
            handleClose={handleClose}
            variant={variant}
            message={message}
            />
        <SignInForm/>
        </div>
    )
}

综合演示

请参阅此CodeSandbox.io 演示,了解其工作的综合演示,以及您拥有的损坏组件,您可以在两者之间切换。

您的答案解决了问题中描述的问题,但是变量open始终false
2021-05-30 08:11:28
也许我应该在另一个问题中问它。谢谢你的回答。
2021-05-30 08:11:28
我没有发现我的错误,因为open对我来说总是错误的但是 !!variant 一开始是错误的,然后它变成了正确
2021-06-03 08:11:28
@para008 查看更新的演示。它现在应该完全工作,并在variant未设置进行模拟
2021-06-12 08:11:28
@para008 我添加了一个全面的演示来证明它可以正常工作。
2021-06-21 08:11:28

SnackbarContentWrapper你需要改变

<IconButton
  key="close"
  aria-label="Close"
  color="inherit"
  className={classes.close}
  onClick={onClose} // change this
>

<IconButton
  key="close"
  aria-label="Close"
  color="inherit"
  className={classes.close}
  onClick={() => onClose()} // to this
>

这样它只会在您单击时触发操作。

或者,您可以将handleClosein 输入SignInContainer

const handleClose = () => (reason) => {
  if (reason === 'clickaway') {
    return;
  }
  setSnackBarState(false)
};

一样的。

谢谢,对我有用。这就是我要找的 onClick={() => onClose} 。
2021-05-25 08:11:28
正是对我有用的。简单而有效的答案。非常感谢。
2021-06-04 08:11:28
onClick={() => onClose} 和 onClick={onClose} 的区别?
2021-06-10 08:11:28
有人可以告诉我谷歌什么才能理解 onClick={() => onClose} 和 onClick={onClose} 之间的区别吗?谢谢!
2021-06-12 08:11:28
谷歌搜索“在反应中绑定内联函数”的效果应该会让你到达你需要去的地方。您将必须了解 js 中的“this”和柯里化函数,这是一个相当大的漏洞。
2021-06-19 08:11:28

您必须在 onClick 中链接一个事件。此外,点击函数必须接收事件。看例子

export default function Component(props) {

    function clickEvent (event, variable){
        console.log(variable);
    }

    return (
        <div>
            <IconButton
                key="close"
                aria-label="Close"
                color="inherit"
                onClick={e => clickEvent(e, 10)}
            >
        </div>
    )
}

您可以通过在函数内使用钩子来防止此错误

在像这样调用 handleFunction 之前,您需要添加一个事件:

function SingInContainer() {
..
..
handleClose = () => {
}

return (
    <SnackBar
        open={open}
        handleClose={() => handleClose}
        variant={variant}
        message={message}
        />
    <SignInForm/>
  )
}