Home > Net >  React Update Navbar When User Updates Profile
React Update Navbar When User Updates Profile

Time:09-08

I have a React Application which just contains Authtentication Functionality. I created a dashboard as a home page which displays users email, and a navbar which checks if user is logged in or not. If user logged in, it displays a dropdown with a title user's email(dropdown items: update profile/log out) else, it just displays a log in button.

In the Update Profile page, User can change password and email.

My problem is, if user chanhges email, navbar do not update itself but dashboard do.

App.js:

import "./style.css";
import Navigation from "./Navbar";
import Signup from "./Signup";
import { AuthProvider } from "../contexts/AuthContext";
import Dashboard from "./Dashboard";
import Login from "./Login";
import Logout from "./Logout";
import ForgotPassword from "./ForgotPassword";
import PrivateRoute from "./PrivateRoute";
import UpdateProfile from "./UpdateProfile";

function App() {
  return (
    <Router >
      <AuthProvider>
        <Navigation />
        <Routes>
          <Route
            path="/"
            element={
              <PrivateRoute>
                <Dashboard />
              </PrivateRoute>
            }
          ></Route>
          <Route
            path="/update-profile"
            element={
              <PrivateRoute>
                <UpdateProfile />
              </PrivateRoute>
            }
          ></Route>
          <Route path="/signup" element={<Signup />} />
          <Route path="/login" element={<Login />} />
          <Route path="/logout" element={<Logout />} />
          <Route path="/forgot-password" element={<ForgotPassword />} />
        </Routes>
      </AuthProvider>
    </Router>
  );
}

export default App;

Navbar.js:

import Container from "react-bootstrap/Container";
import Nav from "react-bootstrap/Nav";
import Navbar from "react-bootstrap/Navbar";
import NavDropdown from "react-bootstrap/NavDropdown";
import { useAuth } from "../contexts/AuthContext";
import { useEffect, useState } from "react";

const Navigation = () => {
  const { currentUser } = useAuth();
  const [email, setEmail] = useState("");

  return (
    <div>
      <Navbar bg="light" expand="lg">
        <Container>
          <Navbar.Brand>
            <Nav.Link as={Link} to="/">
              Commercial
            </Nav.Link>
          </Navbar.Brand>
          <Navbar.Toggle aria-controls="responsive-navbar-nav" />
          <Navbar.Collapse id="responsive-navbar-nav">
            <Nav>
              <NavDropdown
                title="Man"
                className="dropdown"
                id="basic-nav-dropdown"
                renderMenuOnMount={true}
              >
                <NavDropdown.Item>Shirt</NavDropdown.Item>
                <NavDropdown.Item>Jean</NavDropdown.Item>
              </NavDropdown>
              <NavDropdown
                title="Woman"
                id="basic-nav-dropdown"
                className="dropdown"
                renderMenuOnMount={true}
              >
                <NavDropdown.Item>Dress</NavDropdown.Item>
                <NavDropdown.Item>Skirt</NavDropdown.Item>
              </NavDropdown>
            </Nav>
          </Navbar.Collapse>
          <Navbar.Collapse
            className="justify-content-end"
            id="responsive-navbar-nav"
          >
            <Nav>
              {currentUser ? (
                <NavDropdown
                title={currentUser.email}
                className="dropdown"
                id="basic-nav-dropdown"
                renderMenuOnMount={true}>
                  <Nav.Link as={Link} to="/update-profile">
                    Go Profile
                  </Nav.Link>
                  <Nav.Link as={Link} to="/logout">
                    Log Out
                  </Nav.Link>
                </NavDropdown>
              ) : (
                <Nav.Link as={Link} to="/login">
                  Login
                </Nav.Link>
              )}
            </Nav>
          </Navbar.Collapse>
        </Container>
      </Navbar>
    </div>
  );
};

export default Navigation;

Dashboard.js:

import { Card, Button, Alert, Container } from "react-bootstrap";
import { useAuth } from "../contexts/AuthContext";

const Dashboard = () => {
  const { currentUser } = useAuth();

  return (
    <Container className="mt-5 d-flex align-items-center justify-content-center">
      <div className="w-100" style={{ maxWidth: "400px" }}>
        <Card>
          <Card.Body>
            <h2 className="text-center mb-4">Profile</h2>
            <strong>Email</strong> {currentUser.email}
          </Card.Body>
        </Card>
      </div>
    </Container>
  );
};

export default Dashboard;

I think I should re-render the navbar every time the page is loaded. How can I do that, or is there another solutions? PS:I have not any functional error/bug. Everything works fine.

EDIT: I am sharing other codes, may be this can help:

AuthContext.js:

import React, { useContext, useState, useEffect } from "react";
import { auth } from "../firebase";

const AuthContext = React.createContext();

export function useAuth() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const [currentUser, setCurrentUser] = useState();
  const [loading, setLoading] = useState(true);

  const signup = (email, password) => {
    return auth.createUserWithEmailAndPassword(email, password);
  };

  const login = (email, password) => {
    return auth.signInWithEmailAndPassword(email, password)
  }

  const logout = () => {
    return auth.signOut();
  }

  const resetPassword = (email) => {
    return auth.sendPasswordResetEmail(email);
  }

  const updateEmail = (email) => {
    return currentUser.updateEmail(email);
  }

  const updatePassword = (password) => {
    return currentUser.updatePassword(password);
  }

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      setCurrentUser(user);
      setLoading(false);
    });
    return unsubscribe;
  }, []);

  const value = {
    currentUser,
    login, 
    signup,
    logout,
    resetPassword,
    updateEmail,
    updatePassword
  };
  return (
    <AuthContext.Provider value={value}>
      <div></div>
      {!loading && children}
    </AuthContext.Provider>
  );
}

PrivateRoute.js:

import { Navigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";

const PrivateRoute = ({ children }) => {
  const { currentUser } = useAuth();
  return currentUser ? children : <Navigate to="/login" />;
};

export default PrivateRoute;

UpdateProfile.js:

import { Form, Button, Card, Container, Alert } from "react-bootstrap";
import { useAuth } from "../contexts/AuthContext";
import { Link, useNavigate } from "react-router-dom";

const UpdateProfile = () => {
  const emailRef = useRef();
  const passwordRef = useRef();
  const passwordConfirmRef = useRef();
  const { currentUser, updateEmail, updatePassword } = useAuth();

  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);
  const navigator = useNavigate();

  async function handleSubmit(e) {
    e.preventDefault();
    if (passwordRef.current.value !== passwordConfirmRef.current.value) {
        return setError("Passwords do not match");
    }

    const promises = [];
    setLoading(true);
    if(emailRef.current.value !== currentUser.email){
      promises.push(updateEmail(emailRef.current.value));
    }
    if(passwordRef.current.value){
      promises.push(updatePassword(passwordRef.current.value));
    }

    Promise.all(promises).then(() => {
      navigator("/");
    }).catch(() =>{
      setError("Failed to update Account");
    }).finally(() =>{
      setLoading(false);
    })
  }

  return (
    <Container className="mt-5 d-flex align-items-center justify-content-center">
      <div className="w-100" style={{ maxWidth: "400px" }}>
        {error && <Alert variant="danger">{error}</Alert>}
        <Card>
          <Card.Body>
            <h2 className="text-center mb-4">Update Profile</h2>
            <Form onSubmit={handleSubmit}>
              <Form.Group id="email">
                <Form.Label>Email</Form.Label>
                <Form.Control
                  type="email"
                  ref={emailRef}
                  defaultValue={currentUser.email}
                  required
                />
              </Form.Group>
              <Form.Group id="password">
                <Form.Label>Password</Form.Label>
                <Form.Control
                  type="password"
                  ref={passwordRef}
                  placeholder="Leave blank to keep the same"
                />
              </Form.Group>
              <Form.Group id="passwordConfirm">
                <Form.Label>Password Confirmation</Form.Label>
                <Form.Control
                  type="password"
                  ref={passwordConfirmRef}
                  placeholder="Leave blank to keep the same"
                />
              </Form.Group>
              <Button disabled={loading} type="submit" className="w-100 mt-3">
                Update
              </Button>
            </Form>
          </Card.Body>
        </Card>
        <div className="w-100 text-center mt-2">
          <Link to="/" style={{ textDecoration: "none" }}>
            Cancel
          </Link>
        </div>
      </div>
    </Container>
  );
};

export default UpdateProfile;

CodePudding user response:

From what I can see here it looks like the currentUser object reference is possibly mutated by Firebase. In other words, the currentUser reference in the AuthProvider component only updates when the authentication status changes, but not when user properties like their email are updated.

Since it seems that when changing routes the Dashboard component is rerendered and "sees" the updated currentUser.email value you could create a layout route component that renders the Navigation component.

Example:

import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";

const PrivateRoute = () => {
  const { currentUser } = useAuth();
  return currentUser ? <Outlet /> : <Navigate to="/login" replace />;
};

...

import { Outlet } from "react-router-dom";

export const Layout = () => (
  <>
    <Navigation />
    <Outlet />
  </>
);

function App() {
  return (
    <Router >
      <AuthProvider>
        <Routes>
          <Route element={<Layout />}> // <-- Navbar renders with routes
            <Route element={<PrivateRoute />}>
              <Route path="/" element={<Dashboard />} />
              <Route path="/update-profile" element={<UpdateProfile />} />
            </Route>
            <Route path="/signup" element={<Signup />} />
            <Route path="/login" element={<Login />} />
            <Route path="/logout" element={<Logout />} />
            <Route path="/forgot-password" element={<ForgotPassword />} />
          </Route>
        </Routes>
      </AuthProvider>
    </Router>
  );
}

An alternative might be to add an additional "state" to the AuthProvider component for marking "updates" to trigger rerenders.

Example:

export function AuthProvider({ children }) {
  const [currentUser, setCurrentUser] = useState();
  const [loading, setLoading] = useState(true);
  const [update, setUpdate] = useState(0);

  ...

  const updateEmail = async (email) => {
    const result = await currentUser.updateEmail(email);
    setUpdate(c => c   1);
    return result;
  }

  const updatePassword = async (password) => {
    const result = await currentUser.updatePassword(password);
    setUpdate(c => c   1);
    return result;
  }

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      setCurrentUser(user);
      setLoading(false);
    });
    return unsubscribe;
  }, []);

  const value = {
    currentUser,
    login, 
    signup,
    logout,
    resetPassword,
    updateEmail,
    updatePassword
  };

  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
}

CodePudding user response:

I think your Navbar "Navigation" Component is not re-rendering on route change.

You could use redux or context api for state management so that when email updates it is updated in the redux or context api store.

So, your Navbar state should update as it will be taking its value from the redux or context api store and if that value changes the state will update in turn the function will re-render

I am not sure if there are other solutions to this.

  • Related