Reputation: 125
I have a navbar component that has a submenu. Once logged in, the submenu of the navbar should change. I used the hook useContext, but it doesn't refresh the navbar component when the user logs. It works fine when I refresh the page. Where is my code problem?
APP COMPONENT
import React, { useState, useEffect } from "react";
import logo from "./assets/logoBusca.png";
import "./App.css";
import { Route } from "wouter";
import Home from "./components/Home";
import Login from "./components/Login";
import Post from "./components/Post";
import NavbarUser from "./components/NavbarUser";
import { AuthContext } from "./context/AuthContext";
import logic from "../src/logic";
function App() {
const [user, setUser] = useState(false);
useEffect(() => {
(async () => {
const loggedIn = await logic.isUserLoggedIn;
if (loggedIn) setUser(true);
})();
}, [user]);
return (
<AuthContext.Provider value={user}>
<div className="App">
<header className="App-header">
<div className="images">
<div className="logo">
<a href="/">
<img src={logo} alt="logo" />
</a>
</div>
<div className="user_flags">
<NavbarUser />
</div>
</div>
</header>
<Route path="/">
<Home />
</Route>
<Route path="/login">
<Login />
</Route>
<Route path="/nuevabusqueda">
<Post />
</Route>
</div>
</AuthContext.Provider>
);
}
export default App;
NAVBAR COMPONENT
import React, { useContext } from "react";
import userIcon from "../../assets/userIcon.png";
import { AuthContext } from "../../context/AuthContext";
export default function NavbarUser() {
const isAuthenticated = useContext(AuthContext);
return (
<>
{!isAuthenticated ? (
<div className="navbar-item has-dropdown is-hoverable">
<img src={userIcon} alt="user" />
<div className="navbar-dropdown">
<a href="/login" className="navbar-item" id="item_login">
Login
</a>
<hr className="navbar-divider" />
<a href="/registro" className="navbar-item" id="item_register">
Registro
</a>
</div>
</div>
) : (
<div className="navbar-item has-dropdown is-hoverable">
<img src={userIcon} alt="user" />
<div className="navbar-dropdown">
<a href="/datos" className="navbar-item" id="item_login">
Perfil
</a>
<hr className="navbar-divider" />
<a href="/user" className="navbar-item" id="item_register">
Logout
</a>
</div>
</div>
)}
</>
);
}
CONTEXT COMPONENT
import { createContext } from "react";
export const AuthContext = createContext();
LOGIC COMPONENT
import buscasosApi from "../data";
const logic = {
set userToken(token) {
sessionStorage.userToken = token;
},
get userToken() {
if (sessionStorage.userToken === null) return null;
if (sessionStorage.userToken === undefined) return undefined;
return sessionStorage.userToken;
},
get isUserLoggedIn() {
return this.userToken;
},
loginUser(email, password) {
return (async () => {
try {
const { token } = await buscasosApi.authenticateUser(email, password);
this.userToken = token;
} catch (error) {
throw new Error(error.message);
}
})();
},
};
export default logic;
Upvotes: 1
Views: 1681
Reputation: 2885
It seems to me that you are not updating state. Initially your state is false
. After first render, your effect is fired and state is again set to false
. After that your effect don't run anymore, since it depends on state that it should change. No state change - no effect - no state change again. Also, if state will change, it will trigger effect, but you will have no need for this.
Your goal here is to build a system that will:
To do this, you need some way to asynchronously send messages from logic
to react. You can do this with some kind of subscription, like this:
const logic = {
set userToken(token) {
sessionStorage.userToken = token;
},
get userToken() {
if (sessionStorage.userToken === null) return null;
if (sessionStorage.userToken === undefined) return undefined;
return sessionStorage.userToken;
},
get isUserLoggedIn() {
return this.userToken;
},
loginUser(email, password) {
return (async () => {
try {
const { token } = await buscasosApi.authenticateUser(email, password);
this.userToken = token;
this.notify(true);
} catch (error) {
throw new Error(error.message);
}
})();
},
subscribers: new Set(),
subscribe(fn) {
this.subscribers.add(fn);
return () => {
this.subscribers.remove(fn);
};
},
notify(status) {
this.subscribers.forEach((fn) => fn(status));
},
};
function useAuthStatus() {
let [state, setState] = useState("checking");
let setStatus = useCallback(
(status) => setState(status ? "authenticated" : "not_authenticated"),
[setState]
);
useEffect(function () {
return logic.subscribe(setStatus);
}, []);
useEffect(function () {
setStatus(logic.isUserLoggedIn);
}, []);
return state;
}
Notice, that now there is three possible states - 'checking', 'authenticated' and 'not_authenticated'. It is more detailed and will prevent some errors. For example if you would want to redirect user to login page when they are not authenticated.
Upvotes: 1
Reputation: 11305
I think Login component doesn't call setUser(true)
Here's an example how it might work.
const { useState, useEffect, createContext, useContext } = React;
const AuthContext = createContext();
const Login = () => {
const [isAuthenticated, setAuth] = useContext(AuthContext);
const onClick = () => setAuth(true);
return <button disabled={isAuthenticated} onClick={onClick}>Login</button>
}
const App = () => {
const [isAuthenticated] = useContext(AuthContext);
return <div>
<Login/>
<button disabled={!isAuthenticated}>{isAuthenticated ? "Authenticated" : "Not Authenticated"}</button>
</div>;
}
const AuthProvider = ({children}) => {
const [isAuthenticated, setAuth] = useState(false);
return <AuthContext.Provider value={[isAuthenticated, setAuth]}>
{children}
</AuthContext.Provider>;
}
ReactDOM.render(
<AuthProvider>
<App />
</AuthProvider>,
document.getElementById('root')
);
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<div id="root"></div>
Upvotes: 1