I have followed this guide in order to implement a login system to my React TS application. However, I have noticed that I use a NavBar component within my app, whereas his is coded directly in the App.tsx file. This has made it difficult for me to update my NavBar whenever a user logs in. It should automatically update the links to change from 'Login' and 'Sign Up' to '*Username' and 'LogOut'. The links are only updated whenever I refresh my browser. Here is my code for the NavBar component as well as the Login.tsx page. I want to be able to update the NavBar as soon as the user is logged in.
import React, { Component } from 'react';
import {Navbar, Nav, Container, NavDropdown} from 'react-bootstrap';
import plantedLogo from '../Images/Planted-logo-white.png';
import IUser from '../types/user';
import AuthService from '../services/auth-service';
import EventBus from '../common/EventBus';
import authService from '../services/auth-service';
type Props = {};
type State = {
isAdmin: boolean,
currentUser: IUser | undefined,
}
class NavbarComp extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.logOut = this.logOut.bind(this);
this.state = {
isAdmin: false,
currentUser: undefined,
};
}
componentDidMount() {
const user = AuthService.getCurrentUser();
if(user) {
this.setState({
currentUser: user,
isAdmin: user.IsAdmin,
});
}
EventBus.on("logout", this.logOut);
}
componentWillUnmount() {
EventBus.remove("logout", this.logOut);
}
logOut() {
authService.logout();
this.setState({
isAdmin: false,
currentUser: undefined,
});
}
render() {
const { currentUser, isAdmin } = this.state;
return (
<div>
<Navbar className="planted-navbar" variant="dark" expand="lg">
<Container className="navbar-container">
<Navbar.Brand href="home" bsPrefix="planted-navbar-brand">
<img
src={plantedLogo}
width="150px"
// height="70px"
className="d-inline-block align-top"
alt="Restorify logo"
/>
</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="planted-navbar-collapse">
<Nav className="ml-auto">
<NavDropdown title="Missions" id="missions-dropdown">
<NavDropdown.Item href="county-select">Select a County</NavDropdown.Item>
</NavDropdown>
<NavDropdown title="Dashboard" id="trees-dropdown">
<NavDropdown.Item href="dashboard">My Dashboard</NavDropdown.Item>
<NavDropdown.Item href="subscribeToTree">Subscribe To Tree</NavDropdown.Item>
</NavDropdown>
<NavDropdown title="Posts" id="posts-dropdown">
<NavDropdown.Item href="post-tile">Posts</NavDropdown.Item>
<NavDropdown.Item href="posts-to-write">Posts to Write</NavDropdown.Item>
<NavDropdown.Item href="write-a-post">Write a Tree Post</NavDropdown.Item>
{isAdmin && (
<NavDropdown.Item href="login-mission-post" id="login-item">Write a Mission Post</NavDropdown.Item>
)}
</NavDropdown>
<Nav.Link href="leaderboard" id="leaderboard-link">Leaderboard</Nav.Link>
<Nav.Link href="about" id="about-link">About</Nav.Link>
{currentUser ? (
<div className='account-links'>
<Nav.Link className="justify-content-end" href="profile" id="profile-link">{currentUser.Username}</Nav.Link>
<Nav.Link className="justify-content-end" href="home" id="logout-link" onClick={this.logOut}>LogOut</Nav.Link>
</div>
) : (
<div className='account-links'>
<Nav.Link className="justify-content-end" href="login" id="login-link">Login</Nav.Link>
<Nav.Link className="justify-content-end" href="register" id="register-link">Sign Up</Nav.Link>
</div>
)}
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
</div>
);
}
}
export default NavbarComp;
Here is code for Login.tsx
import '../../styles/login.css'
import { Formik, Field, Form, ErrorMessage } from "formik";
import * as Yup from "yup";
import { Navigate } from "react-router-dom";
import { Component } from "react";
import AuthService from "../../services/auth-service";
type Props = {};
type State = {
redirect: string | null,
username: string,
password: string,
loading: boolean,
message: string
};
export default class Login extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.handleLogin = this.handleLogin.bind(this);
this.state = {
redirect: null,
username: "",
password: "",
loading: false,
message: ""
};
}
componentDidMount() {
const currentUser = AuthService.getCurrentUser();
if (currentUser) {
this.setState({ redirect: "/profile" });
};
}
// componentWillUnmount(): void {
// window.location.reload();
// }
validationSchema() {
return Yup.object().shape({
username: Yup.string().required("This field is required!"),
password: Yup.string().required("This field is required!"),
});
}
handleLogin(formValue: { username: string; password: string }) {
const { username, password } = formValue;
this.setState({
message: "",
loading: true
});
AuthService.login(username, password).then(
() => {
this.setState({
redirect: "/profile"
});
},
error => {
const resMessage =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
this.setState({
loading: false,
message: resMessage
});
}
);
}
render() {
if (this.state.redirect) {
return <Navigate to={this.state.redirect} />
}
const { loading, message } = this.state;
const initialValues = {
username: "",
password: "",
};
return (
<div className="col-md-12">
<div className="card card-container">
<img
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
alt="profile-img"
className="profile-img-card"
/>
<Formik
initialValues={initialValues}
validationSchema={this.validationSchema}
onSubmit={this.handleLogin}
>
<Form>
<div className="login-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="login-form-group">
<label htmlFor="password">Password</label>
<Field name="password" type="password" className="form-control" />
<ErrorMessage
name="password"
component="div"
className="alert alert-danger"
/>
<br />
</div>
<div className="login-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>
{message && (
<div className="login-form-group">
<div className="alert alert-danger" role="alert">
{message}
</div>
</div>
)}
</Form>
</Formik>
</div>
</div>
);
}
}
The profile page is loaded as soon as user logs in, however the NavBar does not update.
I was able to solve this issue through employing an event listener.
const eventBus = {
on(event: string, callback: EventListener) {
document.addEventListener(event, (e) => callback(e));
},
dispatch(event: string, data?: any) {
document.dispatchEvent(new CustomEvent(event, { detail: data }));
},
remove(event: string, callback: EventListener) {
document.removeEventListener(event, callback);
},
};
export default eventBus;
I then turned this event listener on for "logged-in" in my NavbarComp.tsx file:
class NavbarComp extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.logOut = this.logOut.bind(this);
this.loggedIn = this.loggedIn.bind(this);
...//
componentDidMount() {
const user = AuthService.getCurrentUser();
if(user) {
this.setState({
currentUser: user,
isAdmin: user.IsAdmin,
});
}
EventBus.on("logout", this.logOut);
EventBus.on("logged-in", this.loggedIn);
}
componentWillUnmount() {
EventBus.remove("logout", this.logOut);
EventBus.remove("logged-in", this.loggedIn);
}
...//
loggedIn() {
const user = AuthService.getCurrentUser();
if(user) {
this.setState({
currentUser: user,
isAdmin: user.IsAdmin,
});
}
}
render() {
...//
}
}
export default NavbarComp;
This event is then dispatched within my auth-service.ts file inside the login method:
login(username: string, password: string) {
return axios
.post(API_URL + "AccountLogin", {
username,
password
})
.then(response => {
// if(response.data.accessToken) {
localStorage.setItem("user", JSON.stringify(response.data));
eventBus.dispatch("logged-in");
// }
return response.data;
});
}