Asynchronous Function in useEffect() hook

25 Views Asked by At

I was trying to develop a web app in React with Express server and PostgreSQL database also using the OpenWeatherMap API.

I configured an authentication strategy based on JWT Token on the server and created the various registration and access endpoints.

The endpoints work and, especially in the login one, a JWT token is generated, which will then be saved in the localStorage on the client side.

  • index.js (back-end)
const jwtStrategy = new JWTStrategy.Strategy(
  {
    jwtFromRequest: JWTStrategy.ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: process.env.JWT_SECRET,
  },
  async (jwtPayload, done) => {
    try {
      const user = await findUserById(jwtPayload.user.id);
      if (user) {
        return done(null, user);
      } else {
        return done(null, false);
      }
    } catch (error) {
      console.error("Error finding user by ID:", error);
      return done(error, false);
    }
  }
);

passport.use(jwtStrategy);

// Signup endpoint
app.post("/api/signup", async (req, res) => {
  try {
    const { name, username, email, password } = req.body;
    // Verify if user already exists
    const existingUser = await findUserByEmail(email);
    if (existingUser) {
      return res.status(400).json({ message: "Email already exists" });
    }
    // Password hashing
    const hashedPassword = await bcrypt.hash(password, 10);
    // Create new user
    const newUser = await signUp(name, username, email, hashedPassword);
    res.status(201).json({ message: "User created successfully" });
  } catch (error) {
    console.error("Error signing up:", error);
    res.status(500).json({ message: "Internal server error" });
  }
});

// Login endpoint
app.post("/api/login", async (req, res) => {
  try {
    const { email, password } = req.body;
    // Verify if user already exists
    const user = await findUserByEmail(email);
    if (!user) {
      return res.status(401).json({ message: "User not found" });
    }
    // Verify password match
    const passwordMatch = await bcrypt.compare(password, user.password);
    if (!passwordMatch) {
      return res.status(401).json({ message: "Invalid email or password" });
    }
    // Token JWT creation
    const token = jwt.sign({ user: user }, process.env.JWT_SECRET, {
      expiresIn: "1h",
    });
    res.status(200).json({ token: token });
  } catch (error) {
    console.error("Error logging in:", error);
    res.status(500).json({ message: "Internal server error" });
  }
});

I also created an /api/validateToken endpoint (using passport.authenticate()) that does nothing other than return a success message and the user object if the token is valid

Everything works great regarding the backend, but I'm having some problems with rendering elements in the frontend.

In my Header component, I wanted to show the two buttons Login and Register if the user is not authenticated and the two buttons Favorite Cities and Logout if the user is authenticated.

Initially, I tried the following approach:

  • Header.jsx
import React from "react";
import { Link } from "react-router-dom";

import "../styles/Header.css";

import { validateToken, logOut } from "../client-utils";

function Header() {
  const token = localStorage.getItem("token");
  const [loading, setLoading] = React.useState(true);
  const [isTokenValid, setIsTokenValid] = React.useState(null);

  React.useEffect(() => {
    validateToken(token, setIsTokenValid, setLoading);
  }, [token]);

  function handleLogout() {
    logOut(token, setIsTokenValid);
  }

  return (
    <header>
      {loading && (
        <div className="loading-container">
          <div className="loading-spinner"></div>
        </div>
      )}
      <nav>
        <ul>
          {isTokenValid ? (
            <>
              <li>
                <Link to="/">Preferred Cities</Link>
              </li>
              <li>
                <button onClick={handleLogout}>Logout</button>
              </li>
            </>
          ) : (
            <>
              <li>
                <Link to="/signup">Sign Up</Link>
              </li>
              <li>
                <Link to="/login">Sign In</Link>
              </li>
            </>
          )}
        </ul>
      </nav>
    </header>
  );
}

export default Header;

With validateToken() and logOut() declare as follows:

async function validateToken(token, setIsTokenValid, setLoading) {
  try {
    const response = await axios.get(
      "http://localhost:5000/api/validateToken",
      {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }
    );
    console.log(response.data);
    setIsTokenValid(true);
  } catch (err) {
    console.log("Errore nella validazione del token");
    setIsTokenValid(false);
  } finally {
    setLoading(false);
  }
}

async function logOut(token, setIsTokenValid) {
  try {
    const response = await axios.get("http://localhost:5000/api/logout", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    console.log(response.data);
    localStorage.removeItem("token");
    setIsTokenValid(false);
  } catch (err) {
    console.log(err);
  }
}

The logout logic works correctly and elements are rendered correctly upon logout.
But after authenticating, however, I am forced to refresh the page to see the updated elements (i.e. the Preferred Cities and Logout buttons).

So I tried this other approach in useEffect hook:

React.useEffect(() => {
    async function checkToken() {
      if (token) {
        await validateToken(token, setIsTokenValid, setLoading);
      } else {
        setLoading(false);
      }
    }

    checkToken();
  }, [token]);

But unfortunately, it still needs refreshing to display the items correctly.

Does anyone know how I can solve this problem? I've been on it for over 4 hours, with no results.

Thanks in advance to anyone who can help me.

1

There are 1 best solutions below

1
Scramjet On

Just putting variables in the dependency array is not enough.
useEffect is only executed when the component renders. Here the value in token will change, but that change will not trigger a rerender, since it is not part of the components state, or parent state.
For useEffect to notice a change the component needs to render.

Unlike events, Effects are caused by rendering itself rather than a particular interaction.

See the docs for more info. Also would suggest reading through it all. Would be helpful in getting an idea of the usage and pitfalls as well.

So you can maybe store the response in a context (or some state management library) at the top level, for the component tree to rerender and use this value.