Search code examples
reactjsreact-hooksreact-router-dom

Update navbar after success login or logout redirection using shared React hook


I have a simple authentication app in React, the problem is that when the user logs in, he is redirected to his profile but my navbar component is not rendering correctly. Only applies the changes after I refresh the page but not when is directly redirected by the request.

App Component:

function App() {

  const auth = useAuth();

  return (
    <>
      <Router>
        {auth.auth ? <Navbar/> : <NotAuthNavbar/>}
        <Routes>
            <Route path="/" element={<Dashboard/>}/>
            <Route path="/login" element={<Login/>}/>
            <Route path="/signup" element={<Register/>}/>
            <Route path="/profile"
                  element={
                    <PrivateRoute>
                      <Profile/>
                    </PrivateRoute>
                  }
            />
            <Route path="/confirm-register/:tokenConfirm" element={<ConfirmRegister/>}/>
            <Route path="/registered" element={<RegisterMessage/>}/>
        </Routes>
      </Router>
    </>
  );
}

export default App;

Auth Custom Hook:

const useAuth = () => {

    const [auth,setAuth] = useState(null);
    const [user,setUser] = useState({});

    const isAuth = async() => {
        await axios.get('http://localhost:5000/api/logged-user/',{withCredentials:true})
        .then(res => {
            setUser(res.data);
            setAuth(true);
        })
        .catch(error => {
            setAuth(false);
        });
    }


    useEffect(() => {
        isAuth();
    },[auth])

    return{
        auth:auth,
        user:user
    }
}
export default useAuth;

Navbar and NotAuthNavbar:

///Navbar.jsx
const Navbar = () => {

    const auth = useAuth();
    const navigate = useNavigate();

    const logout = async() =>{
        await axios.get("http://localhost:5000/api/logout/",{withCredentials:true})
        .then(res => {
            console.log(res.data);
            navigate('/login');
            return;
        })
    }

    return(
        <section>
            <div className="navbar">
                <ul className="navbar-menu">
                    <li><Link to={"/dashboard"}>Dashboard</Link></li>
                    <li><Link to={"/profile"}>Welcome {auth.user.username}</Link></li>
                    <li><button type='button' onClick={logout}>Logout</button></li>
                </ul>
            </div>
            <Outlet />
        </section>
        )
}

export default Navbar;

/// NotAuthNavbar.jsx
const NotAuthNavbar = () => {


    return(
        <section>
            <div className="navbar">
                <ul className="navbar-menu">
                    <li><Link to={"/dashboard"}>Dashboard</Link></li>
                    <li><Link to={"/signup"}>Sign Up</Link></li>
                    <li><Link to={"/login"}>Sign In</Link></li>
                </ul>
            </div>
            <Outlet />
        </section>
        )
}

export default NotAuthNavbar;

When user log in, I redirect to his profile with Navigate by react-router-dom.

I don't know how to update the navbar after login and logout. I can't stop thinking there is a small details that I don't see, but I'm have been stucked in this for few hours.

Thanks in advance.

EDIT

This is the function of login

const login = async(e) => {

        e.preventDefault();

        await axios.post("http://localhost:5000/api/signin/",{
            email:email.email,
            password:password.password
        },{
            withCredentials:true,
        })
        .then(res => {
            setIsSubmitted(true);
            if(res.status === 200){
                alert('!LOGGED');
                navigate('/profile');
                return res.data;
            }
        })
        .catch(error => {
            let parsedErrors = [];
            parsedErrors = JSON.parse(error.request.response);

            setHandleErrors(parsedErrors);

            setIsSubmitted(true);
        })
    }

Solution

  • Issue

    React hooks don't share state.

    Solution

    Move the useAuth state into a React context and provide that to the app. The useAuth hook should then use the useContext hook to access the single auth state.

    Example:

    Create an AuthContext, provider, and hook.

    import { createContext, useContext, useEffect, useState } from 'react';
    
    const AuthContext = createContext({
      auth: null,
      setAuth: () => {},
      user: null,
    });
    
    export const useAuth = () => useContext(AuthContext);
    
    const AuthProvider = ({ children }) => {
      const [auth, setAuth] = useState(null);
      const [user, setUser] = useState(null);
    
      useEffect(() => {
        const isAuth = async () => {
          try {
            const res = await axios.get(
              'http://localhost:5000/api/logged-user/',
              { withCredentials: true }
            );
          
            setUser(res.data);
          } catch(error) {
            setUser(null);
          };
        };
    
        isAuth();
      }, [auth]);
    
      return (
        <AuthContext.Provider value={{ auth, setAuth, user }}>
          {children}
        </AuthContext.Provider>
      );
    };
    
    export default AuthProvider;
    

    Wrap the App component with the AuthProvider component.

    import AuthProvider from "../path/to/AuthContext";
    
    ...
    
    <AuthProvider>
      <App />
    </AuthProvider>
    

    Now that there's a single auth and user state the useAuth hooks all reference the same state.

    App

    function App() {
      const { auth } = useAuth();
    
      return (
        <Router>
          {auth ? <Navbar /> : <NotAuthNavbar />}
          <Routes>
            <Route path="/" element={<Dashboard />} />
            <Route path="/login" element={<Login />} />
            <Route path="/signup" element={<Register />} />
            <Route
              path="/profile"
              element={(
                <PrivateRoute>
                  <Profile />
                </PrivateRoute>
              )}
            />
            <Route path="/confirm-register/:tokenConfirm" element={<ConfirmRegister />} />
            <Route path="/registered" element={<RegisterMessage />} />
          </Routes>
        </Router>
      );
    }
    

    Navbar

    Access the setAuth updater function to set auth false upon successful logout.

    const Navbar = () => {
      const { setAuth, user } = useAuth();
      const navigate = useNavigate();
    
      const logout = async () => {
        const res await axios.get(
          "http://localhost:5000/api/logout/",
          { withCredentials: true }
        );
        console.log(res.data);
        setAuth(false);
        navigate('/login');
      }
    
      return(
        <section>
          <div className="navbar">
            <ul className="navbar-menu">
              <li><Link to={"/dashboard"}>Dashboard</Link></li>
              <li><Link to={"/profile"}>Welcome {user.username}</Link></li>
              <li><button type='button' onClick={logout}>Logout</button></li>
            </ul>
          </div>
          <Outlet />
        </section>
      );
    };
    

    Component using login function

    Use the useAuth hook to access the setAuth updater function and set auth true upon successful authentication.

    const { setAuth } = useAuth();
    
    ...
    
    const login = async (e) => {
      e.preventDefault();
    
      try {
        const res = await axios.post(
          "http://localhost:5000/api/signin/",
          {
            email: email.email,
            password: password.password
          },
          {
            withCredentials: true,
          }
        );
    
        if (res.status === 200) {
          alert('!LOGGED');
          setAuth(true);
          navigate('/profile');
        }
      } catch(error) {
        setHandleErrors(JSON.parse(error.request.response));
      } finally {
        setIsSubmitted(true);
      }
    }
    

    Note: It's anti-pattern to mix async/await with Promise chains. Generally you should select one or the other. I've used async/await with try/catch to handle rejected promises and other errors that may occur.