React JS: Rendered fewer hooks than expected. This may be caused by an accidental early return statement
Asked Answered
T

4

6

I am trying to make authentication for a React JS project but, I am getting the following error:

Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

I was reading other questions (like this and this) but I guess my situation is different since I set all the useState once the component is created.

I have used different approaches to get this done but neither of them worked for me.

I added the methods in the ContextWrapper component like this:

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>;
};

And then this is my Login component:

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>
  );
};

Here I created this sandbox to reproduce the error that happens when login() method is triggered. So, what am I doing wrong?

Trudge answered 24/1, 2020 at 15:28 Comment(1)
The return <Redirect to="/" />; before the useEffect in Login is the problem.Antepenult
S
10

Move your condition under the useEffect.

useEffect(() => {})

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

Because your if condition was affecting the subsequent hook below it, as it wouldn't run if context.token is true. This violates the Rules of Hooks.

Pro Tip: Add ESlint Plugin as suggested in the React-Docs to get those warnings/errors while you code.

Spoon answered 24/1, 2020 at 16:8 Comment(4)
well... maybe that works, but now I am getting Maximum update depth exceeded. due infinite reloads...Trudge
@Trudge well, that's another issue isn't it? your Home component have side effects inside the body of the component(context.verify()), move it to useEffect and also rethink your architectureSpoon
I guess it is another question ... So thanks for solving my first issue.Trudge
@Trudge No problem, the answer is right here though, you CANT have any side effects inside the component body, so no network calls, no state updates, move them to useEffect. and a better way will be to just not render the route if the user is not loggedSpoon
P
1

Move the redirect logic after the hook.

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

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

In your home component you are redirecting user if userIsValid id not true. And this filed will always be undefined as it is not defined in context. Add this field in the context and try.

Hope this will solve the issue.

Planula answered 24/1, 2020 at 16:19 Comment(0)
P
1

In my case, l use useWindowDimension to get height within a View's style. This mistake costs me more than 3 days of endless search until l realized useWindowDimension is a hook too. So the proper way to use it is this:

 const window = useWindowDimension();

 return (
    <View style={{ height: window.height, }} />
 ); 
Pithos answered 14/7, 2021 at 13:15 Comment(0)
R
-1

The error from console tells you exactly where the problem is. You can not create hooks conditionally. This hook is a problem and needs to appear before return statement. This probably means you should change loading condition to work as expected.

useEffect(() => {
    context.setLoading(false);
}, []);
Rodarte answered 24/1, 2020 at 16:4 Comment(2)
where I create a hook conditionally?Trudge
You have if statement with return before hook from my comment. It means that hook will be created only after your if condition (context.token) is true.Springhalt

© 2022 - 2024 — McMap. All rights reserved.