-1

I'm new to ReactJS and am building a basic application. Here I'm using protected router and implementing an authorization mechanism with useContext and local storage. The aim is to redirect users that are not logged in to the Login Page if they attempt to access Dashboard.

After I do log in, the access token is saved to local storage and account info is saved in auth context. Then I go to Dashboard and I reload the page. I implemented a useEffect hook to check for token in local storage and I thought that when I reload at the Dashboard page, the auth provider would check for the token and return a result that I'm authenticated. However it doesn't work as expected so I am redirected to Login page (Although the useEffect callback was triggered)

enter image description here

Below is my code:

src\components\App\index.js

import { Routes, Route } from 'react-router-dom';

import Login from '../Login';
import Signup from '../Signup';
import GlobalStyles from '../GlobalStyles';
import ThemeProvider from 'react-bootstrap/ThemeProvider';
import RequireAuth from '../RequireAuth';
import Layout from '../Layout';
import Dashboard from '../Dashboard';
import Account from '../Account';

function App() {
    return (
        <GlobalStyles>
            <ThemeProvider>
                <Routes>
                    <Route path="/" element={<Login />} />
                    <Route path="/login" element={<Login />} />
                    <Route path="/signup" element={<Signup />} />
                    <Route element={<RequireAuth />}>
                        <Route path="/" element={<Layout />}>
                            <Route path="/dashboard" element={<Dashboard />} />
                            <Route path="/account" element={<Account />} />
                        </Route>
                    </Route>
                </Routes>
            </ThemeProvider>
        </GlobalStyles>
    );
}

export default App;

src\components\RequireAuth\index.js

import { useLocation, Navigate, Outlet } from 'react-router-dom';
import useAuth from '../../hooks/useAuth';

function RequireAuth() {
    const { auth } = useAuth();
    const location = useLocation();

    return auth?.user ? (
        <Outlet />
    ) : (
        <Navigate to={{ pathname: '/', state: { from: location } }} replace />
    );
}

export default RequireAuth;

src\hooks\useAuth.js

import { useContext } from 'react';
import { AuthContext } from '../context/AuthProvider';

function useAuth() {
    return useContext(AuthContext);
}

export default useAuth;

src\context\AuthProvider.js

import { useEffect } from 'react';
import { createContext, useState } from 'react';

import api from '../helper/api';

const AuthContext = createContext({});

function AuthProvider({ children }) {
    const [auth, setAuth] = useState({});

    useEffect(() => {
        const apiHelper = new api();
        apiHelper.getAccountInfo().then((response) => {
            setAuth(response.data);
        });
    }, []);

    console.log(auth.user);

    return (
        <AuthContext.Provider value={{ auth, setAuth }}>
            {children}
        </AuthContext.Provider>
    );
}

export { AuthContext, AuthProvider };

src\components\Login\index.js

import { useState, useRef, useEffect } from 'react';
import clsx from 'clsx';

import { Form, FormGroup, Button } from 'react-bootstrap';
import Alert from 'react-bootstrap/Alert';
import FloatingLabel from 'react-bootstrap/FloatingLabel';
import { useNavigate } from 'react-router';

import styles from './style.module.scss';
import logo from '../../assets/images/logo.png';
import LoadingSpinner from '../LoadingSpinner';

import api from '../../helper/api';
import useAuth from '../../hooks/useAuth';

const Login = () => {
    const usernameRef = useRef();
    const errorRef = useRef();

    const [state, setState] = useState({
        username: '',
        password: ''
    });
    const { username, password } = state;
    const [error, setError] = useState();
    const [loading, setLoading] = useState(false);

    useEffect(() => {
        usernameRef.current.focus();
    }, []);

    const { setAuth } = useAuth();

    const navigate = useNavigate();

    const submitForm = (event) => {
        event.preventDefault();
        setLoading(true);
        const apiHelper = new api();
        apiHelper
            .login({
                username,
                password
            })
            .then((response) => {
                setLoading(false);
                setError();
                setAuth({ user: response.data.user });
                localStorage.setItem('token', response.data.token);
                navigate('/dashboard');
            })
            .catch((error) => {
                setLoading(false);
                setError(error.response.data.message);
                usernameRef.current.focus();
            });
    };

    return (
        /** Some hmtml */
    );
};

export default Login;

A video on how the error occurs: https://streamable.com/b1cp1t

Can anyone tell me where I'm wrong and how to fix it? Many thanks in advance!

namln-hust
  • 309
  • 3
  • 9
  • Does this answer your question? [React Context API - persist data on page refresh](https://stackoverflow.com/questions/53453861/react-context-api-persist-data-on-page-refresh) – SuleymanSah Sep 09 '22 at 18:14
  • Sorry, I don't see where any localStorage is used. Where is the `AuthProvider` component rendered? Can you edit the post to include all relevant code used for your [mcve]? – Drew Reese Sep 09 '22 at 18:17
  • I can't find where you used localstorage. There's redux-persist to persist data after reloading but if you mean to use localstorage, you have to pass auth info you get from localstorage as a default context value. – Meerkat Sep 09 '22 at 18:19
  • Not sure what token you are using but as with anything, I would be weary of storing a key that has access to any PII data. If it's a JWT, just be sure you know what you're doing with regards to expiry times. – AttemptedMastery Sep 09 '22 at 19:15
  • I've added my login component to show how token and auth state is save. Also, I added a console.log in src\context\AuthProvider.js and a video to clarify the problem. It seeems the useEffect does not take effect or somehow the context is lost after reloading and redering take effect. – namln-hust Sep 10 '22 at 11:46

1 Answers1

0

you can think about a <Route> kind of like an if statement, if its path matches the current URL, it renders its element! since the path of your first route in the list is "/" to the login page matches the need of the router it will redirect you there.

so, if you will delete this line:

<Route path="/" element={<Login />} />

and let the <RequireAuth /> take care of the path="/" it will check first if the user is logged in, if so let him continue. if not it will redirect to "/login"

Yarin Barnes
  • 211
  • 1
  • 6
  • I added a link to a video describing my situation. Like I said, I successfully got access to the Dashboard, the token is saved in local storage and the user info is saved in auth state (auth context). But when I reload at Dashboard, the state couldn't be maintained. See the video for more. – namln-hust Sep 10 '22 at 11:48
  • does the video take care on those suggestions described above? – Yarin Barnes Sep 11 '22 at 07:13
  • I tried your solution but it does not work, even the api keeps calling for an infinite times – namln-hust Sep 11 '22 at 17:01