2

I am trying to make it so that when a user clicks on a certain Link (Upload Component), it will redirect them to to the login component and am not sure how to accomplish this task in React. I was directed to another answered question, but it hasn't helped me as I am still confused on what I need to do with my own set up. I understand I need to make my own protected route (maybe), but I saw others accessing useContext and I do not have any file with context. I am using Version 6 in react dom. I am also using React router and redux in my project so I know I need to access the state somehow, just not sure how to wrap my mind around it so if anyone could help, I would appreciate it. The user is being stored in local storage with JWT authentication.

App.js:

import React, {useState, useEffect, useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom'; //Switch was replaced by Routes in react-router-dom v6
import './App.css';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import Home from './components/pages/Home';
import Uploads from './components/pages/Uploads';
import Account from './components/pages/Account';
import Login from './components/pages/Login';
import LogOut from './components/Logout';
import Register from './components/pages/Register';
import Profile from './components/pages/Profile';
import EditProfile from './components/pages/EditProfile';
import BoardAdmin from './components/pages/BoardAdmin';
import BoardModerator from './components/pages/BoardModerator';
import BoardUser from './components/pages/BoardUser';
import {logout} from './slices/auth';
import EventBus from './common/EventBus';

const App = () => {
    const [showModeratorBoard, setShowModeratorBoard] = useState(false);
    const [showAdminBoard, setShowAdminBoard] = useState(false);
    const {user: currentUser} = useSelector((state) => state.auth);
    const dispatch = useDispatch();

    useEffect(() => {
        if (currentUser) {
            setShowModeratorBoard(currentUser.roles.includes('ROLE_MODERATOR'));
            setShowAdminBoard(currentUser.roles.includes('ROLE_ADMIN'));
        } else {
            setShowModeratorBoard(false);
            setShowAdminBoard(false);
        }
    }, [currentUser]);

    return (
        <>
            <Router>
                <Navbar />
                <Routes>
                    <Route path='/' element={<Home />} />
                    <Route path='/create/upload' element={<Uploads />} />
                    <Route path='/my-account' element={<Account />} />
                    <Route path='/login' element={<Login />} />
                    <Route path='/logout' element={<LogOut />} />
                    <Route path='/register' element={<Register />} />
                    <Route path='/profile' element={<Profile />} />
                    <Route path='/profile/edit' element={<EditProfile />} />
                    <Route path='/user' element={<BoardUser />} />
                    <Route path='/mod' element={<BoardModerator />} />
                    <Route path='/admin' element={<BoardAdmin />} />
                </Routes>
                <Footer />
            </Router>
        </>
    );
};

export default App;

Auth.js:

import {createSlice, createAsyncThunk} from '@reduxjs/toolkit';
import {setMessage} from './messages';
import AuthService from '../services/auth.service';
const user = JSON.parse(localStorage.getItem('user'));
export const register = createAsyncThunk(
    'auth/register',
    async ({username, email, password}, thunkAPI) => {
        try {
            const response = await AuthService.register(username, email, password);
            thunkAPI.dispatch(setMessage(response.data.message));
            return response.data;
        } catch (error) {
            const message =
                (error.response &&
                    error.response.data &&
                    error.response.data.message) ||
                error.message ||
                error.toString();
            thunkAPI.dispatch(setMessage(message));
            return thunkAPI.rejectWithValue();
        }
    }
);
export const login = createAsyncThunk(
    'auth/login',
    async ({username, password}, thunkAPI) => {
        try {
            const data = await AuthService.login(username, password);
            return {user: data};
        } catch (error) {
            const message =
                (error.response &&
                    error.response.data &&
                    error.response.data.message) ||
                error.message ||
                error.toString();
            thunkAPI.dispatch(setMessage(message));
            return thunkAPI.rejectWithValue();
        }
    }
);
export const logout = createAsyncThunk('auth/logout', async () => {
    await AuthService.logout();
});
const initialState = user
    ? {isLoggedIn: true, user}
    : {isLoggedIn: false, user: null};
const authSlice = createSlice({
    name: 'auth',
    initialState,
    extraReducers: {
        [register.fulfilled]: (state, action) => {
            state.isLoggedIn = false;
        },
        [register.rejected]: (state, action) => {
            state.isLoggedIn = false;
        },
        [login.fulfilled]: (state, action) => {
            state.isLoggedIn = true;
            state.user = action.payload.user;
        },
        [login.rejected]: (state, action) => {
            state.isLoggedIn = false;
            state.user = null;
        },
        [logout.fulfilled]: (state, action) => {
            state.isLoggedIn = false;
            state.user = null;
        },
    },
});
const {reducer} = authSlice;
export default reducer;

It looks like adding the if statement in my upload file is making it so that a logged in user can access the h1 element that is in the file, but not an unauthenticated user. So its working to an extent, just not redirecting back to login.

Upload.jsx:

import React from 'react';
import {Link} from 'react-router-dom';
import {useSelector} from 'react-redux';
function Uploads() {
    const {user: currentUser} = useSelector((state) => state.auth);
    if (!currentUser) {
        return <Link to='/login' />;
    }
    return (
        <>
            <h1 className='page'>UPLOADS</h1>
        </>
    );
}

export default Uploads;
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
Stephen Aranda
  • 173
  • 2
  • 14
  • Hi Stephen, "I was directed to another answered question, but it hasn't helped me as I am still confused on what I need to do with my own set up." Can you share the other question(s)/answer(s) that you were directed to and explain why they didn't work? – Drew Reese Apr 08 '22 at 07:53
  • Hello Drew! https://stackoverflow.com/questions/66289122/how-to-create-a-protected-route . That is the link, and it looks very useful, I just am no sure how to implement it since I dont have a context file like in this post – Stephen Aranda Apr 08 '22 at 07:56
  • Ok, just wanted to check before I linked you to that same post. You don't need to use a React context. Anything that holds a global state that is accessible will work. You could even create a private route component that checks an auth token on each route access and doesn't use any local state at all. You meantion using Redux, so `useSelector` could access any of your "authentication" state for the auth check purpose. – Drew Reese Apr 08 '22 at 07:59
  • const {user: currentUser} = useSelector((state) => state.auth); that is a line of code that I use to see if there is an authenticated user. So I tried `if (!currentUser) {return ; }` and added it to my upload component, but still when I click the link it doesn't redirect to login for some reason – Stephen Aranda Apr 08 '22 at 08:04
  • Oh, you'd use that logic in a protected route. When a user is accessing a path, check the `currentUser` value and either redirect them to the `"/login"` path or render the protected component. Just render the `Link` components normally. – Drew Reese Apr 08 '22 at 08:06
  • So I am on the right track. By the way thank you so much for the help, this is helping me understand what I need to do more than you know.. Any idea why, when I add that line in my Upload component, it doesn't redirect to login? Instead it allows the path to be loaded (not completely as it is not showing the data in that component) – Stephen Aranda Apr 08 '22 at 08:12
  • It looks like its working to an extent, since it doesn't load the data associated with the component, but it is not redirecting to my login component for some reason – Stephen Aranda Apr 08 '22 at 08:13

1 Answers1

1

Rendering the Link doesn't imperatively navigate, use the Navigation component.

Example:

import React, { useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';

function Uploads() {
  const { user: currentUser } = useSelector((state) => state.auth);

  if (!currentUser) {
    return <Navigate to='/login' replace />;
  }
  return (
    <>
      <h1 className='page'>UPLOADS</h1>
    </>
  );
}

export default Uploads;

Typically you would protect the route instead. If you wanted to do that:

import { Navigate, Outlet } from 'react-router-dom';
import { useSelector } from 'react-redux';

const PrivateRoutes = () => {
  const { user: currentUser } = useSelector((state) => state.auth);
  return currentUser 
    ? <Outlet />
    : <Navigate to="/login" replace />;
}

...

<Routes>
  <Route path='/' element={<Home />} />
  <Route element={<PrivateRoutes />}>
    <Route path='/create/upload' element={<Uploads />} />
    ... any other protected routes ...
  </Route>
  ... other unprotected routes ...
</Routes>
Drew Reese
  • 165,259
  • 14
  • 153
  • 181