Search code examples
reactjstypescriptreact-tsx

How to update NavBar after user logs in?


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.


Solution

  • 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;
                });
        }