1

I am trying to "copy" my real world project with CodeSandbox (here). I have a login form and when login, it sets a cookie with auth data. When the <LoginPage> component mounts, it should check for that cookie and redirect back to <HomePage> component if that exists.

In my case that doesn't work as expected since it waits until I write something in the <LoginPage> form.

According to the hookrouter documentation navigate() should work as react-router-dom <Redirect /> component.

So, this is the project src folder structure:

  • src
    • index.js
    • components
      • pages
        • Home
          • index.js
        • Login
          • index.js
    • context
      • Auth
        • auth.js
        • index.js

Code:

App: src/index.js

import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import { useRoutes, A, navigate } from "hookrouter";
import { useCookies } from "react-cookie";

import HomePage from "./components/pages/Home";
import LoginPage from "./components/pages/Login";

import AuthContextWrapper from "./context/Auth";

const Logout = () => {
  const [cookies, setCookie, removeCookie] = useCookies(["token"]);
  useEffect(() => {
    console.log("Logout triggered");
    removeCookie("token");
    navigate("/", true);
  }, []);
  return <p>Closing session</p>;
};

const routes = {
  "/": () => <HomePage />,
  "/login": () => <LoginPage />,
  "/logout": () => <Logout />
};

const App = () => {
  const Router = useRoutes(routes);
  return Router ? (
    <AuthContextWrapper>{Router}</AuthContextWrapper>
  ) : (
    <h1>
      Error 404: <A href="/">Go back</A>
    </h1>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

<HomePage />: src/components/pages/Home/index.js

import React, { Fragment, useContext } from "react";
import { A } from "hookrouter";
import AuthContext from "../../../context/Auth/auth";

const HomePage = () => {
  const context = useContext(AuthContext);
  console.log("Home context", context);
  return (
    <Fragment>
      <A href="/login">Login page</A>
      {context.token.length ? <A href="/logout">Logout</A> : ""}
    </Fragment>
  );
};

export default HomePage;

<LoginPage />: src/components/pages/Login/index.js

import React, { useState, useContext, useEffect } from "react";
import AuthContext from "../../../context/Auth/auth";
import { navigate } from "hookrouter";

const LoginPage = () => {
  const context = useContext(AuthContext);
  const [emailState, setEmailState] = useState("");
  const [passwordState, setPasswordState] = useState("");
  const [statusState, setStatusState] = useState({
    color: "black",
    status: ""
  });

  useEffect(() => {
    console.log("Login context:", context);
    if (context.token) {
      navigate("/");
    }
  }, []);

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={emailState}
        onChange={e => setEmailState(e.target.value)}
        placeholder="Email"
      />
      <input
        value={passwordState}
        onChange={e => setPasswordState(e.target.value)}
        placeholder="Password"
        type="password"
      />
      <p style={{ color: statusState.color }}>{statusState.status}</p>
      <button>Login</button>
    </form>
  );

  async function handleSubmit(e) {
    e.preventDefault();
    setStatusState({
      color: "blue",
      status: "Validating"
    });
    await context.login(emailState, passwordState);
    if (context.error) {
      setStatusState({
        color: "red",
        status: context.error
      });
    }
    if (context.token) {
      navigate("/");
    }
  }
};

export default LoginPage;

<AuthContextWrapper />: src/context/Auth/index.js

import React, { useEffect } from "react";
import Context from "./auth";
import { useCookies } from "react-cookie";

const AuthContextWrapper = props => {
  const [cookies, setCookie] = useCookies(["token"]);
  const defaultValue = {
    token: "",
    error: "",
    loading: false,
    login: async (email, password) => {
      setTimeout(() => {
        defaultValue.loading = true;
        if (password !== "123") {
          defaultValue.error = "Wrong credentials";
        } else {
          defaultValue.error = "";
          defaultValue.token = "jwtencodedtoken$123";
          setCookie("token", defaultValue.token);
        }
      }, 1000);
      defaultValue.loading = false;
    }
  };
  useEffect(() => {
    if (cookies.token) {
      defaultValue.token = cookies.token;
    }
  });
  return (
    <Context.Provider value={defaultValue}>{props.children}</Context.Provider>
  );
};

export default AuthContextWrapper;

So, I guess my head just exploded... This is a live example living inside CodeSandbox. Something wrong with hooks maybe (I am still learning them).

Why this does not work? What am I doing wrong? Any comments are appreciated.

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
Maramal
  • 3,145
  • 9
  • 46
  • 90

2 Answers2

2

Okay so I updated your code sandbox here.

Changes to src/context/Auth/index.js

Your context provider was a bit awkward. Your going to have a bad time if you try and just manipulating an object directly like that with your login function. If you want to see updates bind them to state and pass them in as values to your provider.

Changes to src/components/pages/Login/index.js

Not necessary for that handleSubmit function to be async, also how you set this up can be problematic without making the login function a promise. You end up returning early on your await instead of after the timeout.

Your better off reacting to those context updates inside the useEffect hook with proper dependencies. This way you can make necessary updates in this component whenever the context updates.

apachuilo
  • 341
  • 3
  • 6
1

This is a dumb solution, go with @apachuilo's answer. Just leaving it here for reference's sake and because I found it interesting that adding a timeout somehow makes it work

The problem appears to be that you context.token value evaluates empty in the useEffect hook of the login function. If you add a timeout to your useEffect function it does have time to retrieve the context value and your redirect will work:

useEffect(() => {
  const timer = setTimeout(() => {
    if (context.token) {
      navigate("/");
    }
  }, 100);
  return () => clearTimeout(timer);
}, []);
etarhan
  • 4,138
  • 2
  • 15
  • 29