1

Any help or advice on this issue would be much appreciated!

I am creating an app with react router and TypeScript and I am using an AuthContext store my user data. I am having an issue when trying to protect some of my routes with a RequireAuth component because it seems that the RequireAuth component is called / rendered / mounted (I'm not sure what the appropriate term would be here) before the AuthContext. Therefore, my user data is always empty and I am stuck in a loop of logging in and being redirected to the login page.

I am sure that the issue is propably something to do with the order in which I mount my components but I am not sure what I'm missing.

Any hints or suggestions would be very welcome!

App.tsx

import { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthContextProvider } from './contexts/AuthContext';
import { useFuncContext } from './contexts/FuncContext';
import { PlaylistContextProvider } from './contexts/PlaylistContext';
import RequireAuth from './components/RequireAuth';
import LandingPage from './pages/LandingPage';
import HomePage from './pages/HomePage';
import Setup from './pages/Setup';
import NotFound from './pages/NotFound';
import Loader from './components/Loader';
import Popup from './components/Popup';
import ErrorAlert from './components/ErrorAlert';

function App() {
  const func = useFuncContext();

  useEffect(() => {
    if (func?.error) {
      console.log(func?.error);
      const errorTimeout = setTimeout(() => func?.setError(''), 3000);
      return () => clearTimeout(errorTimeout);
    }
    else if (func?.popupMessage) {
      console.log(func?.popupMessage);
      const popupTimeout = setTimeout(() => func?.setPopupMessage(''), 3000);
      return () => clearTimeout(popupTimeout);
    }
  }, [func]);

  return (
    <>
      <div className="App">
        {func?.popupMessage && <Popup message={func?.popupMessage} />}
        {func?.error && <ErrorAlert errorMessage={func?.error} />}
      </div>
      {func?.isLoading ? (
        <Loader className='loading' />
      ) : (
        <AuthContextProvider>
          <PlaylistContextProvider>
            <Router>
              <Routes>
                <Route path='/' element={<LandingPage />} />
                <Route path='/home' element={<RequireAuth><HomePage /></RequireAuth>} />
                <Route path='/setup' element={<RequireAuth><Setup /></RequireAuth>} />
                <Route path='*' element={<RequireAuth><NotFound /></RequireAuth>} />
              </Routes>
            </Router>
          </PlaylistContextProvider>
        </AuthContextProvider>
      )}
    </>
  );
}

export default App;

AuthContext.tsx

import { useState, useEffect, createContext, useContext } from 'react'
import { useFuncContext } from './FuncContext'

type authDataType = {
    id?: string,
    username?: string,
    displayName?: string,
    profileUrl?: string,
    profileImage?: string
}

type contextType = {
    authData: authDataType,
    onLogout: () => void
}

type contextProviderProps = {
    children: React.ReactElement
}

const AuthContext = createContext<contextType | undefined>(undefined);

const AuthContextProvider = ({ children }: contextProviderProps) => {
    const [authData, setAuthData] = useState<authDataType | {}>({});
    const func = useFuncContext();

    useEffect(() => {
        console.log('AuthContext')
        const getLoggedInUser = async () => {
            try {
                const response = await fetch('/auth/user', {
                    method: 'GET',
                    headers: {
                        Accept: 'application/json'
                    },
                    credentials: 'include'
                })
                const data = await response.json();
                if (!response.ok || data.status === 'error') {
                    func?.setError(data.message);
                    throw new Error(data.message);
                }
                setAuthData(data);
                return data;
            } catch (error) {
                throw new Error('Cannot get logged in user')
            }
        }
        getLoggedInUser()
    }, [])

    const onLogout = () => setAuthData({});

    return (
        <AuthContext.Provider value={{ authData, onLogout }}>
            {children}
        </AuthContext.Provider>
    )
}

const useAuthContext = () => useContext(AuthContext);

export { AuthContextProvider, useAuthContext }; 

RequireAuth.tsx

import React, { useEffect } from 'react'
import { Navigate } from 'react-router-dom';
import { useAuthContext } from '../contexts/AuthContext';
import { useFuncContext } from '../contexts/FuncContext';

type reqAuthProps = {
    children: React.ReactElement
}

const RequireAuth = ({ children }: reqAuthProps) => {
    const auth = useAuthContext();
    const func = useFuncContext()

    useEffect(() => {
        console.log('RequireAuth')
        if (!auth?.authData.id) func?.setError('Please log in to access this page')
    }, [])

    if (!auth?.authData.id) {
        return <Navigate to='/' replace />
    }

    return children;
}

export default RequireAuth; 
Mz_22
  • 33
  • 2
  • Basic gist is that the `auth` state is initially `{}` so the `!auth?.authData.id` evaluates truthy and the redirect to `"/"` is effected. The `RequireAuth` component should wait until the initial auth check completes prior to rendering either the redirect or the `children` prop. I'd suggest using either `null` or `undefined` as the initial `auth` state value so it's easier to check and conditionally early return a loading indicator/etc, e.g. `null === null` and `undefined === undefined` are always true, but `{} === {}` is never true. – Drew Reese Mar 10 '23 at 10:40

0 Answers0