Home > Enterprise >  How do I implement a Logout functionality from a React Router Link to imports an array?
How do I implement a Logout functionality from a React Router Link to imports an array?

Time:03-18

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>
  • Related