React JS:渲染的钩子比预期的要少。这可能是由于意外提前退货声明造成的

Posted

技术标签:

【中文标题】React JS:渲染的钩子比预期的要少。这可能是由于意外提前退货声明造成的【英文标题】:React JS: Rendered fewer hooks than expected. This may be caused by an accidental early return statement 【发布时间】:2020-05-10 22:57:25 【问题描述】:

我正在尝试为 React JS 项目进行身份验证,但出现以下错误:

渲染的钩子比预期的要少。这可能是由于意外的提前退货声明造成的。

我正在阅读其他问题(例如 this 和 this),但我想我的情况有所不同,因为我在创建组件后设置了所有 useState

我使用了不同的方法来完成这项工作,但它们都不适合我。

我在ContextWrapper 组件中添加了这样的方法:

import React,  useState, useEffect  from "react";
import  useCookies  from "react-cookie";
import jwt from "jsonwebtoken";
import  fetchLogin, fetchVerify  from "../fake-server";
import Context from "./context";

export default ( children ) => 
  const [token, setToken] = useState(null);
  const [message, setMessage] = useState(null);
  const [loading, setLoading] = useState(false);
  const [cookies, setCookie, removeCookie] = useCookies(["token"]);

  useEffect(() => 
    console.log(cookies);
    if (cookies.token) 
      setToken(cookies.token);
    
  , []);

  useEffect(() => 
    if (token) 
      setCookie("token", JSON.stringify(token),  path: "/" );
     else 
      removeCookie("token");
    

    console.log(token);
  , [token]);

  function login(email, password) 
    fetchLogin(email, password)
      /*.then(response => response.json())*/
      .then(data => 
        const decoded = jwt.decode(data.token);
        const token = 
          token: data.token,
          ...decoded
        ;
        setToken(token);
      )
      .catch(error => 
        console.log("error", error);
        setMessage(
          status: 500,
          text: error
        );
      );
  

  function verify() 
    fetchVerify(cookies.token)
      /*.then(response => response.json())*/
      .then(data => 
        setToken(data);
      )
      .catch(error => 
        setToken(null);
        setMessage(
          status: 500,
          text: error
        );
      );
  

  function logout() 
    setToken(false);
  

  const value = 
    login,
    verify,
    logout,
    token,
    message,
    setMessage,
    loading,
    setLoading
  ;

  return <Context.Provider value=value>children</Context.Provider>;
;

然后这是我的Login 组件:

import React,  useState, useContext, useEffect  from "react";
import 
  Grid,
  Card,
  Typography,
  CardActions,
  CardContent,
  FormControl,
  TextField,
  Button
 from "@material-ui/core";
import  Redirect  from "react-router-dom";
import  makeStyles  from "@material-ui/core/styles";
import Context from "../context/context";

const useStyles = makeStyles(theme => (
  root: 
    height: "100vh",
    display: "flex",
    justifyContent: "center",
    alignContent: "center"
  ,
  title: 
    marginBottom: theme.spacing(2)
  ,
  card: ,
  formControl: 
    marginBottom: theme.spacing(1)
  ,
  actions: 
    display: "flex",
    justifyContent: "flex-end"
  
));

export default ( history, location ) => 
  const context = useContext(Context);
  const classes = useStyles();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  if (context.token) 
    return <Redirect to="/" />;
  

  useEffect(() => 
    context.setLoading(false);
  , []);

  function onSubmit(e) 
    e.preventDefault();
    context.login(email, password);
  

  return (
    <Grid container className=classes.root>
      <Card className=classes.card>
        <form onSubmit=onSubmit>
          <CardContent>
            <Typography variant="h4" className=classes.title>
              Login
            </Typography>
            <FormControl className=classes.formControl fullWidth>
              <TextField
                type="text"
                variant="outlined"
                label="Email"
                value=email
                onChange=e => setEmail(e.target.value)
              />
            </FormControl>
            <FormControl className=classes.formControl fullWidth>
              <TextField
                type="password"
                variant="outlined"
                label="Password"
                value=password
                onChange=e => setPassword(e.target.value)
              />
            </FormControl>
          </CardContent>
          <CardActions className=classes.actions>
            <Button type="submit" variant="contained" color="secondary">
              Login
            </Button>
          </CardActions>
        </form>
      </Card>
    </Grid>
  );
;

这里我创建了this sandbox 来重现触发login() 方法时发生的错误。那么,我做错了什么?

【问题讨论】:

登录中useEffect 之前的return &lt;Redirect to="/" /&gt;; 是问题所在。 【参考方案1】:

将你的条件移到useEffect下。

useEffect(() => )

if (context.token) 
  return <Redirect to="/" />;

因为您的if 条件影响了它下面的后续挂钩,因为如果context.token 为真,它将不会运行。 这违反了Rules of Hooks。

专业提示:按照 React-Docs 中的建议添加 ESlint 插件,以便在您编码时获取这些警告/错误。

【讨论】:

好吧......也许这行得通,但现在我得到Maximum update depth exceeded.由于无限重新加载...... @Maramal 好吧,那是另一个问题,不是吗?您的 Home 组件在组件主体内有副作用(context.verify()),将其移至 useEffect 并重新考虑您的架构 我猜这是另一个问题......所以感谢您解决我的第一个问题。 @Maramal 没问题,答案就在这里,你不能在组件体内有任何副作用,所以没有网络调用,没有状态更新,将它们移动到 useEffect。如果用户未登录,更好的方法是不渲染路由【参考方案2】:

在我的例子中,我使用useWindowDimension 来获得View 风格内的高度。这个错误花费了我超过 3 天的无休止搜索,直到我意识到 useWindowDimension 也是一个钩子。所以正确的使用方法是这样的:

 const window = useWindowDimension();

 return (
    <View style= height: window.height,  />
 ); 

【讨论】:

【参考方案3】:

将重定向逻辑移到钩子之后。

useEffect(() => 
    context.setLoading(false);
  , []);

  if (context.token) 
    return <Redirect to="/" />;
  

如果userIsValid id 不是true,则在您的主组件中重定向用户。并且此文件将始终未定义,因为它未在context 中定义。在上下文中添加此字段并尝试。

希望这能解决问题。

【讨论】:

【参考方案4】:

来自控制台的错误会告诉您问题出在哪里。您不能有条件地创建挂钩。这个钩子是个问题,需要出现在return语句之前。这可能意味着您应该更改加载条件以按预期工作。

useEffect(() => 
    context.setLoading(false);
, []);

【讨论】:

我在哪里有条件地创建一个钩子? 我的评论中你有 if 语句在钩子之前返回。这意味着只有在您的 if 条件(context.token)为真之后才会创建钩子。

以上是关于React JS:渲染的钩子比预期的要少。这可能是由于意外提前退货声明造成的的主要内容,如果未能解决你的问题,请参考以下文章

渲染的钩子比预期的要少。这可能是由于意外提前退货声明造成的。基于状态的条件渲染组件

React - 出现“渲染的钩子比上一次渲染时更多”错误

React/NextJS 使用 UseEffect 错误:渲染的钩子比上一次渲染期间更多

React Hooks 渲染的钩子比上一次渲染时更多

未处理的拒绝(错误):渲染的钩子比上一次渲染时更多(React Interface GrandStack with ApolloClient)

Next.JS“比上一次渲染时渲染了更多的钩子”