I am trying to implement a logout functionality and am not sure what I need to do to implement it with my current set up. I was able to set up a Login functionality that pulls the correct profile data and once logged in, the Navbar Dropdown Items are updated to display a link to logout. However, I am unsure of how I can add the logout functionality at the moment. I have been following guides but am stuck since nothing I am trying is working, once I hit a the logout link it links to /logout but fails to actually logout. The tutorial I am following claims I can do it via the frontend (I am using JWT).
Navbar.jsx:
import React, {useState, useEffect} from 'react';
import {Link} from 'react-router-dom';
import './Navbar.css';
import Dropdown from './Dropdown';
import {NavItemsList} from './NavItemsList';
import NavItems from './NavItems';
function Navbar() {
const [click, setClick] = useState(false);
const [button, setButton] = useState(true);
const [dropdown, setDropdown] = useState(false);
const handleClick = () => setClick(!click);
const closeMobileMenu = () => setClick(false);
const onm ouseEnter = () => {
if (window.innerWidth < 960) {
setDropdown(false);
} else {
setDropdown(true);
}
};
const onm ouseLeave = () => {
if (window.innerWidth < 960) {
setDropdown(false);
} else {
setDropdown(false);
}
};
return (
<>
<nav className='navbar'>
<div className='navbar-container-whole'>
<div className='left-nav-container'>
{/* Link in react-router-dom essentially replaces a tag.*/}
<Link to='/' className='navbar-logo'>
<img src='/images/logo.png' className='hashtek-logo' alt='logo' />
<h1 className='navbar-name'>HashTek</h1>
</Link>
</div>
{/* .navbar-container will create a div with that class name. */}
<div className='center-nav-container'>
<form action='./' method='get' id='search-form'>
<div class='searchbar'>
<input
class='searchbar_input'
type='search'
name='search'
placeholder='Search..'
/>
<button type='submit' class='searchbar_button'>
<i class='material-icons'>search</i>
</button>
</div>
</form>
</div>
<div className='right-nav-container'>
<ul className={click ? 'nav-menu active' : 'nav-menu'}>
<div className='text-links'>
{/* This line above is for when you are on mobile, and an item is clicked, the nav menu will disappear */}
{NavItemsList.slice(0, 4).map((menu, index) => {
return <NavItems items={menu} key={index} />;
})}
</div>
<div className='logo-links'>
{NavItemsList.slice(4, 6).map((menu, index) => {
return <NavItems items={menu} key={index} />;
})}
</div>
</ul>
<div className='menu-icon' onClick={handleClick}>
<i className={click ? 'fas fa-times' : 'fas fa-bars'} />
</div>
</div>
</div>
</nav>
</>
);
}
export default Navbar;
NavItemList.js:
export const NavItemsList = [{
title: 'Products',
path: '/products',
cName: 'nav-links',
},
{
title: 'Stats',
path: '/stats',
cName: 'nav-links',
},
{
title: 'Contacts',
path: '/contacts',
cName: 'nav-links',
subNav: [{
title: 'About',
path: '/contacts/about',
cName: 'dropdown-link',
menuName: 'contacts-menu',
},
{
title: 'How To',
path: '/contacts/how-to',
cName: 'dropdown-link',
menuName: 'contacts-menu',
},
{
title: 'Developers',
path: '/contacts/developers',
cName: 'dropdown-link',
menuName: 'contacts-menu',
},
{
title: 'Designers',
path: '/contacts/designers',
cName: 'dropdown-link',
menuName: 'contacts-menu',
},
{
title: 'Mentors',
path: '/contacts/mentors',
cName: 'dropdown-link',
menuName: 'contacts-menu',
},
],
},
{
title: 'Services',
path: '/services',
cName: 'nav-links',
subNav: [{
title: 'Streaming',
path: '/services/streaming',
cName: 'dropdown-link',
menuName: 'services-menu',
},
{
title: 'Editing',
path: '/services/editing',
cName: 'dropdown-link',
menuName: 'services-menu',
},
],
},
{
title: Account,
path: '/my-account',
cName: 'nav-links',
subNav: [{
title: 'Login',
path: '/login',
cName: 'dropdown-link',
menuName: 'account-menu',
authenticated: false,
},
{
title: 'Logout',
path: '/logout',
cName: 'dropdown-link',
menuName: 'account-menu',
authenticated: true,
},
{
title: 'Profile',
path: '/profile',
cName: 'dropdown-link',
menuName: 'account-menu',
},
],
},
{
title: Help,
path: '/help',
cName: 'nav-links',
},
];
Login.jsx:
import React, {useState, useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {Link} from 'react-router-dom';
import {Formik, Field, Form, ErrorMessage} from 'formik';
import * as Yup from 'yup';
import {login} from '../../slices/auth';
import {clearMessage} from '../../slices/messages';
const Login = (props) => {
const [loading, setLoading] = useState(false);
const {isLoggedIn} = useSelector((state) => state.auth);
const {message} = useSelector((state) => state.message);
const dispatch = useDispatch();
useEffect(() => {
dispatch(clearMessage());
}, [dispatch]);
const initialValues = {
username: '',
password: '',
};
const validationSchema = Yup.object().shape({
username: Yup.string().required('This field is required!'),
password: Yup.string().required('This field is required!'),
});
const handleLogin = (formValue) => {
const {username, password} = formValue;
setLoading(true);
dispatch(login({username, password}))
.unwrap()
.then(() => {
props.history.push('/profile');
window.location.reload();
})
.catch(() => {
setLoading(false);
});
};
if (isLoggedIn) {
return <Link to='/profile' />;
}
return (
<div className='login-form'>
<div className='card card-container'>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleLogin}
>
<Form>
<div className='form-group'>
<label htmlFor='username'>Username</label>
<Field name='username' type='text' className='form-control' />
<ErrorMessage
name='username'
component='div'
className='alert alert-danger'
/>
</div>
<div className='form-group'>
<label htmlFor='password'>Password</label>
<Field name='password' type='password' className='form-control' />
<ErrorMessage
name='password'
component='div'
className='alert alert-danger'
/>
</div>
<div className='form-group'>
<button
type='submit'
className='btn btn-primary btn-block'
disabled={loading}
>
{loading && (
<span className='spinner-border spinner-border-sm'></span>
)}
<span>Login</span>
</button>
</div>
</Form>
</Formik>
</div>
{message && (
<div className='form-group'>
<div className='alert alert-danger' role='alert'>
{message}
</div>
</div>
)}
</div>
);
};
export default Login;
auth.service.js:
//Authentication Service file. This service uses Axios for HTTP requests and Local Storage for user information & JWT.
// It provides following important functions:
// register(): POST {username, email, password}
// login(): POST {username, password} & save JWT to Local Storage
// logout(): remove JWT from Local Storage
import axios from 'axios';
const API_URL = 'http://localhost:8080/api/auth/';
const register = (username, email, password) => {
return axios.post(API_URL 'signup', {
username,
email,
password,
});
};
const login = (username, password) => {
return axios
.post(API_URL 'login', {
username,
password,
})
.then((response) => {
if (response.data.accessToken) {
localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
});
};
const logout = () => {
localStorage.removeItem('user');
};
const authService = {
register,
login,
logout,
};
export default authService;
auth.js:
// We’re gonna import AuthService to make asynchronous HTTP requests with trigger one or more dispatch in the result.
// – register(): calls the AuthService.register(username, email, password) & dispatch setMessage if successful/failed
// – login(): calls the AuthService.login(username, password) & dispatch setMessage if successful/failed
// – logout(): calls the AuthService.logout().
// setMessage is imported from message slice that we’ve created above.
// We also need to use Redux Toolkit createAsyncThunk which provides a thunk that will take care of the action types and dispatching the right actions based on the returned promise.
//There are 3 async Thunks to be exported:
// register
// login
// logout
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;
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 Account from './components/pages/Account';
import Login from './components/pages/Login';
import Profile from './components/pages/Profile';
import {logout} from './slices/auth';
import EventBus from './common/EventBus';
const App = () => {
const {user: currentUser} = useSelector((state) => state.auth);
const dispatch = useDispatch();
const logOut = useCallback(() => {
dispatch(logout());
}, [dispatch]);
useEffect(() => {
EventBus.on('logout', () => {
logOut();
});
return () => {
EventBus.remove('logout');
};
}, [currentUser, logOut]);
return (
<>
<Router>
<Navbar />
<Routes>
<Route path='/' element={<Home />} />
<Route path='/my-account' element={<Account />} />
<Route path='/login' element={<Login />} />
{currentUser ? (
<Route path='/logout' onClick={logOut} />
) : ( //Did something wrong here I believe. Not sure if I need to include this in another file.
<Route path='/login' element={<Login />} />
)}
<Route path='/profile' element={<Profile />} />
</Routes>
<Footer />
</Router>
</>
);
};
export default App;
I feel like I am close, just missing something or have something in the wrong place. If anyone can help me, I would deeply appreciate it. Sorry if I included too much code, just want everyone to have a good idea of whats going on. Thank you!
CodePudding user response:
It seems you only need a Logout
component that dispatches the logout
action when it mounts, waits for the action to complete, and likely redirect back to a homepage or similar.
Example:
const Logout = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
dispatch(logout())
.then(() => {
navigate("/", { replace: true });
});
}, []);
return <LoadingSpinner />;
};
App
There likely also isn't a string need to conditionally render the routes, just render them normally.
...
import Login from './components/pages/Login';
import Logout from './components/pages/Logout';
...
...
<Router>
<Navbar />
<Routes>
<Route path='/' element={<Home />} />
<Route path='/my-account' element={<Account />} />
<Route path='/login' element={<Login />} />
<Route path='/logout' element={<LogOut />} />
<Route path='/profile' element={<Profile />} />
</Routes>
<Footer />
</Router>