I have a Navigation bar in my project which I call from inside App.js. Based on if I am logged in or not, I want to render different views of NavBar. If logged in, I want the NavBar to have a logout button. And if logged out, I want the NavBar to have login button. I use a token in localStorage to check if I am logged in or not. When logged in, token is present in localStorage. On logout/before login, there is no token key in localStorage. I pass this token as a state to NavBar as shown:
export default function App() {
const [loggedIn, setLoggedIn] = useState(localStorage.getItem("token"));
return (
<div>
<Router>
<Navbar isAuth={loggedIn} />
<Route exact path="/" exact component={Home} />
<Route path="/login" component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
</Router>
</div>
);
}
Now from NavBar component, I use this prop to render different views of NavBar as shown below:
const NavBar = props => {
const classes = useStyles();
if (props.isAuth !== null) {
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={classes.title}>
<Link
href="/"
style={{ textDecoration: "none", color: "white" }}
>
Timetracker
</Link>
</Typography>
<Link href="/" style={{ color: "white" }}>
<Button color="inherit" onClick={auth.logout}>
Logout
</Button>
</Link>
</Toolbar>
</AppBar>
</div>
);
} else {
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={classes.title}>
<Link
href="/"
style={{ textDecoration: "none", color: "white" }}
>
Timetracker
</Link>
</Typography>
<Link href="/login" style={{ color: "white" }}>
<Button color="inherit">Login</Button>
</Link>
</Toolbar>
</AppBar>
</div>
);
}
};
export default NavBar;
The problem is that, the NavBar does not update itself as soon as I login. I have to manually refresh the page in order to render the new NavBar. Similarly on logout too, It does not update itself and updates only on manual refresh. What is the issue and how to solve this?
I found a simple solution:
use a componentDidMount() or useEffect() function which will render automatically upon loading the NavBar page.
Inside this function, use a setInterval() function to continually retrieve the auth status (say, an interval of 5000). This will continually refresh the NavBar, and change the button immediately.
I imagine you would have to put the auth check in the NavBar component itself, instead of using props. I put the specific buttons I wanted to change in a separate component called NavBarUser, which changes 'login | signup' to 'logout' and contains a user avatar. I then inserted this component into the NavBar itself at the appropriate place.
This is what my code looks like:
```
import React, { useState, useEffect } from 'react';
import Avatar from './Avatar';
import { BrowserRouter as Router, Link } from 'react-router-dom';
const NavBarUser = () => {
const [user, setUser] = useState({});
useEffect(() => {
{ /*
setInterval was used in order to refresh the page constantly
in order to have the "logout" button show immediately in place of
"login", as soon as user logs out.
*/}
setInterval(() => {
const userString = localStorage.getItem("user");
const user = JSON.parse(userString);
setUser(user);
}, [])
}, 5000);
const logout = () => {
return localStorage.removeItem("user");
}
if (!user) {
return (
<div className="navbar-nav ml-auto">
<Link to="/login" className="nav-item nav-link">Login</Link> <span
className="nav-item nav-link">|</span> <Link to="/SignUp" className="nav-item nav-
link">Sign Up</Link>
</div>
)
}
if (user) {
return (
<div className="navbar-nav ml-auto">
<Link to="/" className="nav-item nav-link" onClick={logout}>Logout</Link>
<Avatar img="/images/Eat-healthy.jpg" />
</div>
)
}
}
export default NavBarUser;
```
You need to add <Switch> as well. From the documentation:
Renders the first child or that matches the location.
<Switch> is unique in that it renders a route exclusively. In contrast, every <Route> that matches the location renders inclusively.
Just like the following:
<Router>
<Switch>
<Navbar isAuth={loggedIn} />
<Route exact path="/" exact component={Home} />
<Route path="/login" component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
</Switch>
</Router>
Read further here: Router
I hope that helps!
Your app's state won't update if you change the value of the token in localStorage.
You need to make sure you update the state, I've added a sandbox if it helps.
Here's how I solved this issue:
To start, I created a isLoggedIn state in my App class. I gave it a componentDidMount() method that would fetch the login state from a cookie on app start. Then I created globalLogin and globalLogout methods as arrow functions, which set the isLoggedIn state to true or false accordingly. I passed my Nav component the isLoggedIn state as a prop and passed the Login and Nav routes the globalLogin and globalLogout methods. These methods can then be called from Login or Nav with this.props.globalLogout(); or this.props.globalLogin();.
This is a simplified version of my App.js.
class App extends Component {
constructor(props){
super(props);
this.state = {
isLoggedIn: false,
}
}
componentDidMount() {
const token = Cookie.get("token") ? Cookie.get("token") : null;
if (token) {
this.setState({ "isLoggedIn": true });
}
}
globalLogin = () => {
this.setState({ "isLoggedIn": true });
}
globalLogout = () => {
this.setState({ "isLoggedIn": false });
}
render() {
return (
<Router>
<div className="App">
<Nav isLoggedIn={this.state.isLoggedIn} globalLogout={this.globalLogout}/>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/login" exact>
<Login globalLogin={this.globalLogin}/>
</Route>
</Switch>
</div>
</Router>
);
}
}
EDIT: using history.push didn't work in login module above so I added an intermediate to handle props
render() {
const LoginIntermediate = (props) => {
return (
<Login {...props} globalLogin={this.globalLogin}/>
)
}
return (
<Router>
<div className="App">
<Nav isLoggedIn={this.state.isLoggedIn} globalLogout={this.globalLogout}/>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/login" exact component={LoginIntermediate} />
</Switch>
</div>
</Router>
);
}
Related
I am new to React and I am using the Traversy crash course and the extra video about the react router 6+.
My Routes are like
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
return (
<div className="container">
<Router>
<Header
title='sikyona'
onAdd={()=> setShowAddtask(!showAddTask)}
showAdd={showAddTask}
/>
<Routes>
<Route
path='/'
element={
<>
{showAddTask && <AddTask onAdd={addTask} />}
{tasks.length > 0
? <Tasks tasks={tasks} onDelete={deleteTask} />
:<p>add tasks</p>
}
</>
}
/>
<Route path='/about' element={<About/>} />
</Routes>
<Footer />
</Router>
</div>
);
The problem is that when I navigate to the homepage http://localhost:3000/ I first see the About page for a second, and then the homepage (Route path='/'...)
I have "react-router-dom": "^6.4.1",
What is this happening and how can I fix it?
The issue isn't that the "About" page or About component is being rendered when the app is loading or navigating to "/" for the first time. It's that the app is actually on "/" and there's no tasks to display just yet and the UI is rendering the container, Header, the Route for path="/" with the "add tasks" text, and the Footer which renders a link to "/about".
Contrast this rendered UI with the actual "/about" path and About component.
Perhaps the UI/UX is fine for you with regards to this behavior and understanding what exactly is being rendered when, and for what reason. If on the other hand you don't want to see any of the UI until data has been loaded you can tweak the code to render nothing or some loading indicator while the tasks are fetched.
Example:
function App() {
const [showAddTask, setShowAddtask] = useState(false);
const [tasks, setTasks] = useState(); // <-- initially undefined
useEffect(() => {
const getTasks = async () => {
try {
const tasks = await fetchTasks();
setTasks(tasks); // <-- defined and populated
} catch(error) {
// log errors, display message, etc... or ignore
setTasks([]); // <-- defined and empty
}
};
getTasks();
}, []);
...
if (!tasks) {
return null; // <-- return null or loading indicator/spinner/etc
}
return (
<div className="container">
<Router>
<Header
title="hello"
onAdd={() => setShowAddtask(show => !show)}
showAdd={showAddTask}
/>
<Routes>
<Route
path="/"
element={
<>
{showAddTask && <AddTask onAdd={addTask} />}
{tasks.length ? (
<Tasks tasks={tasks} onDelete={deleteTask} />
) : (
<p>add tasks</p>
)}
</>
}
/>
<Route path="/about" element={<About />} />
<Route path="/task-details/:id" element={<TaskDetails />} />
</Routes>
<Footer />
</Router>
</div>
);
}
Try to put your <Route path='/' /> last in the list.
I'm trying to build a dark mode toggle in JS using React.
I'm using Material-UI to design my page and I've managed to make the button change the theme of my whole page when clicked. My issue is that whenever I refresh or change the Routes, the button state doesn't get carried over. Is there anything I'm missing?
This is the code for my button:
export default function ThemeSwitch({ toggleDark, settoggleDark }) {
const handleModeChange = () => {
settoggleDark(!toggleDark);
};
return (
<div className="theme-btn">
<IOSSwitch
checked={toggleDark}
onChange={handleModeChange}
name="toggleDark"
color="default"
/>
</div>
);
}
And this is my App.js:
function App() {
const [toggleDark, settoggleDark] = useState(false);
const myTheme = createTheme({
palette: {
mode: toggleDark ? "dark" : "light",
primary: {
main: "#ff1744",
},
secondary: {
main: "#880e4f",
},
},
});
return (
<Router>
<ThemeProvider theme={myTheme}>
<CssBaseline />
<Navbar />
<ThemeSwitch
toggleDark={toggleDark}
settoggleDark={settoggleDark}
className="theme-btn"
/>
<UserAuthContextProvider>
<Routes>
<Route
path="/"
element={
<ProtectedRoute>
<Hero />
</ProtectedRoute>
}
/>
<Route path="signup" element={<SignUp />} />
<Route path="signin" element={<SignIn />} />
</Routes>
</UserAuthContextProvider>
</ThemeProvider>
</Router>
);
}
In order to maintain the value, you can use localStorage instead of react state.
Whenever you set the state save it in local storge like:
localStorage.setItem("toggleDark", true);
And get the state by using
localStorage.getItem("toggleDark");
Here is my App.js:
function App() {
return (
<BrowserRouter>
<AuthProvider>
<Switch>
<PrivateRoute path="/" component={MainPage} />
<PrivateRoute path="/admin" component={MainPage} />
<Container
className="d-flex align-items-center justify-content-center"
style={{ minHeight: "100vh" }}
>
<div className="w-100" style={{ maxWidth: "400px" }}>
<Route path="/signup" component={Signup} />
<Route path="/login" component={Login} /> //The component part
<Route path="/forgot-password" component={ForgotPassword} />
</div>
</Container>
</Switch>
</AuthProvider>
</BrowserRouter>
);
}
Auth Context for PriveRoute Component from firebase:
export default function PrivateRoute({ component: Component, ...rest }) {
const { currentUser } = useAuth();
return (
<Route
render={(props) => {
return currentUser ? <Admin {...props} /> : <Redirect to="/login" />; // it redirects when the user does not log in.
}}
></Route>
);
Login Component:
export default function Login() {
const emailRef = useRef();
const passwordRef = useRef();
const { login, currentUser } = useAuth();
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const history = useHistory();
if (currentUser) {
history.push("/admin/mainpage");
}
async function handleSubmit(e) {
e.preventDefault();
try {
setError("");
setLoading(true);
await login(
emailRef.current.value + "#myosmail.com",
passwordRef.current.value
);
history.push("/admin/mainpage");
} catch {
setError("Failed to log in");
}
setLoading(false);
}
return (
<>
<Card>
<Card.Body>
<h2 className="text-center mb-4">Log In</h2>
{error && <Alert variant="danger">{error}</Alert>}
<Form onSubmit={handleSubmit}>
<Form.Group id="email">
<Form.Label>Username</Form.Label>
<Form.Control ref={emailRef} required />
</Form.Group>
<Form.Group id="password">
<Form.Label>Password</Form.Label>
<Form.Control type="password" ref={passwordRef} required />
</Form.Group>
<Button disabled={loading} className="w-100" type="submit">
Log In
</Button>
</Form>
<div className="w-100 text-center mt-3">
<Link to="/forgot-password">Forgot Password?</Link>
</div>
</Card.Body>
</Card>
<div className="w-100 text-center mt-2">
Need an account? <Link to="/signup">Sign Up</Link>
</div>
</>
);
}
I want to build a component if the user does not logged in, it can not access to content of my web site so I build the component above. But the problem is that when I opened my page and does not logged in the page redirect me to "/login" but the Login component does not be rendered I do not understand what is the problem. I debugged my component and I saw that my code firstly goes to PrivateRoute component after that it redirected to "/login" but nothing rendered when the page redirected to Login. When I removed PrivateRoutes from App.js my code worked correctly.
Issue
My guess is that you've placed a "/" path first within the Switch component:
<PrivateRoute path="/" component={MainPage} />
The redirect to "/login" works but then the Switch matches the "/" portion and tries rendering this private route again.
Your private route is also malformed, it doesn't pass on all the Route props received.
Solution
Fix the private route component to pass on all props.
export default function PrivateRoute({ component: Component, ...rest }) {
const { currentUser } = useAuth();
return (
<Route
{...rest}
render={(props) => {
return currentUser ? <Admin {...props} /> : <Redirect to="/login" />;
}}
/>
);
}
Within the Switch component path order and specificity matter, you want to order more specific paths before less specific paths. "/" is a path prefix for all paths, so you want that after other paths. Nested routes also need to be rendered within a Switch so only a single match is returned and rendered.
<BrowserRouter>
<AuthProvider>
<Switch>
<Container
className="d-flex align-items-center justify-content-center"
style={{ minHeight: "100vh" }}
>
<div className="w-100" style={{ maxWidth: "400px" }}>
<Switch>
<Route path="/signup" component={Signup} />
<Route path="/login" component={Login} /> //The component part
<Route path="/forgot-password" component={ForgotPassword} />
</Switch>
</div>
</Container>
<PrivateRoute path={["/admin", "/"]} component={MainPage} />
</Switch>
</AuthProvider>
</BrowserRouter>
Update
I'm a bit confused by your private route though, you specify the MainPage component on the component prop, but then render an Admin component within. Typically an more agnostic PrivateRoute component may look something more like:
const PrivateRoute = props => {
const { currentUser } = useAuth();
return currentUser ? <Route {...props} /> : <Redirect to="/login" />;
}
This allows you to use all the normal Route component props and doesn't limit to using just the component prop.
Usages:
<PrivateRoute path="/admin" component={Admin} />
<PrivateRoute path="/admin" render={props => <Admin {...props} />} />
<PrivateRoute path="/admin">
<Admin />
</PrivateRoute>
I also had same problem once. This solved me. Wrap your three routes with a Switch.
<Switch>
<Route path="/signup" component={Signup} />
<Route path="/login" component={Login} />
<Route path="/forgot-password" component={ForgotPassword} />
</Switch>
As the first private route has the root path it will always go to the route. You can use exact for the first private route. But the best way should be placing the first private route
<PrivateRoute path={["/admin", "/"]} component={MainPage} />
at the bottom. So that when there is no match it goes there only.
You are not passing the path to the Route in the ProtectedRoute .
<Route
{...rest} // spread the test here
render={(props) => {
return currentUser ? <Admin {...props} /> : <Redirect to="/login" />; // it redirects when the user does not log in.
}}
></Route>
Also switch the Route of the path as #Drew Reese mentioned .
I hope you are ok.
I'm trying to switch to log out in Navabar when the user is logged in, but when I console log I get isloggedin false, even though the user is logged in and the token is stored in localstorage.
here is my code:
App.js
const App = () => {
const [isLoggedIn, setLoggedIn] = useState(false)
console.log(isLoggedIn)
const handleLogin = token => {
if (!token) return
localStorage.setItem('token', token)
setLoggedIn(true)
}
const handleLogout = () => () => {
setLoggedIn(false)
localStorage.clear()
}
return (
<div className="App">
<Router>
<Navbar isLoggedIn={isLoggedIn} logout={handleLogout} />
<Switch>
<Route
exact path='/'
component={props => (
<Login {...props} onLogin={handleLogin} />
)}></Route>
<Route exact path="/signin" component={Signin}></Route>
<Route exact path="/feed" component={Feed}></Route>
<Route exact path="/withuser" component={Withuser}></Route>
</Switch>
</Router>
</div>
)
}
export default App;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Navbar.js:
const TheNavbar = (props) => {
console.log(props)
return (
<div className="navbar">
<Navbar>
<NavbarBrand>
<NavLink id="logo" tag={Link} exact to="/">
<img src="/img/logounax.png" alt="image" style={{ width: 150 }} />
</NavLink>
</NavbarBrand>
<Nav>
{props.isLoggedIn ? (
<Nav>
<NavLink className="link-pink" tag={Link} exact to="/feed">
Profile
</NavLink>
<NavLink
className="link-pink"
tag={Link}
exact to="/"
onClick={props.logout()}>
Logout
</NavLink>
</Nav>
) : (
<Nav>
<NavLink className="link-pink" tag={Link} exact to="/">
Login
</NavLink>
<NavLink className="link-pink" tag={Link} exact to="/signin">
Signin
</NavLink>
</Nav>
)}
</Nav>
</Navbar>
</div>
)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Any ideas why is this happening?
Thank you in advance.
(Post edited)
I check your code there are some issue. You are trying toke check on handleLogin which will only check when you click on handleLogin. What happen if user refresh the page. It will ideally redirect to dashboard page if token is available. So you have to add useEffect which checks on component mount. Basically you need to add this in your App.js component. It will check when component is mounted first time only and if token is available it will set it
useEffect(() => {
if (localStorage.getItem("token")) {
setLoggedIn(true);
}
}, []);
Just add this snippet in your file. It automatically works if every other things are correct.
You still dont added complete code ;-). But I dont think it will required. This will solve your that problem
I modify your sandbox. If you want to test the functionality type test for both field. Here is the demo
I've got the following structure in my React app, using react-router-dom.
<Router>
<Header/>
<Main>
<AllRoutes> // THIS HANDLES THE SWITCH WITH ALL THE ROUTES
<Switch>
<Route exact path={ROUTES.HOME} component={Home}/>
<Route exact path={ROUTES.ABOUT} component={About}/>
<Route exact path={ROUTES.PRIVACY} component={Privacy}/>
// ETC
</Switch>
</AllRoutes>
</Main>
<Footer/> // <==== FOOTER NEEDS TO KNOW WHICH ROUTE HAS BEEN MATCH
<Router>
QUESTION
Footer needs to know what <Route/> has been match. What is the best pattern to achieve that?
OPTION #1
I found the useRouteMatch hook over on react router docs:
This would kind of work, but I don't think it is good enough for my situation. Because a URL string can match a route and still don't be a valid route at the same time.
For example:
Route: /:language/privacy
Valid route: /en/privacy
Not valid route that would also match: /notALanguage/privacy
Once a route has match, I usually need to check if it is valid before rendering a component page or the 404 page.
Like:
<Route exact path={"/:language/privacy"} render={(routeProps) => {
const possibleLanguage = routeProps.match.params.language;
if (possibleLanguage in LANGUAGES) {
return(
<PrivacyPage lang={possibleLanguage}/>
);
}
else {
return(
<Page404/>
);
}
}}/>
OPTION #2
What I'm thinking about doing is:
App.js calls useLocation. So it always re-render when there is a route change.
I could add a detectRoute function in App.js to do all the route checking beforehand.
And my AllRoutes component wouldn't need a component. I would implement a native JS switch and render the corresponding route.
This way I know upfront which <Route/> is going to match and I can pass it on to <Footer/> or any component that lives outside of the matched <Route/>.
Something like this:
SandBox Link
export default function App() {
console.log("Rendering App...");
const location = useLocation();
// THIS WOULD BE THE detectRoute FUNCTION
// I COULD EVEN USE THE useRouteMatch HOOK IN HERE
const matchedRoute =
location.pathname === ROUTE1
? "ROUTE1"
: location.pathname === ROUTE2
? "ROUTE2"
: "404";
return (
<div>
<div className="App">
<Link to={ROUTE1}>Route 1</Link>
<Link to={ROUTE2}>Route 2</Link>
<Link to={"/whatever"}>Route 404</Link>
</div>
<div>
<AllRoutes matchedRoute={matchedRoute} />
</div>
</div>
);
}
function AllRoutes(props) {
switch (props.matchedRoute) {
case "ROUTE1":
return <Route exact path={ROUTE1} component={Page1} />;
case "ROUTE2":
return <Route exact path={ROUTE2} component={Page2} />;
default:
return <Route exact path={"*"} component={Page404} />;
}
}
It works. But I would like to know if there's a proper way of doing this, 'cause this seems a bit weird and there might be something out there that was specifically designed for this.
Generally you want to either:
Wrap the components together
Create another switch to route them (and pass match params)
I put together a somewhat comprehensive example of the options. Hope that helps!
import React from "react";
import "./styles.css";
import { Switch, Link, Route, BrowserRouter } from "react-router-dom";
const hoc = (Component, value) => () => (
<>
<main>
<Component />
</main>
<Footer value={value} />
</>
);
const Wrapper = ({ component: Component, value }) => (
<>
<main>
<Component />
</main>
<Footer value={value} />
</>
);
const WrapperRoute = ({ component, value, ...other }) => (
<Route
{...other}
render={props => <Wrapper component={component} value={value} {...props} />}
/>
);
const Footer = ({ value }) => <footer>Footer! {value}</footer>;
const Header = () => <header>Header!</header>;
const Another = () => <Link to="/onemore">One More!</Link>;
const Home = () => <Link to="/other">Other!</Link>;
const OneMore = () => <Link to="/">Home!</Link>;
const Other = () => <Link to="/another">Another!</Link>;
export default () => (
<BrowserRouter>
<Header />
<Switch>
{/* You could inline it! */}
<Route
path="/another"
render={() => (
<>
<main>
<Another />
</main>
<Footer value="" />
</>
)}
/>
{/* You could use a custom route component (that uses an HOC or a wrapper) */}
<WrapperRoute
component={OneMore}
path="/onemore"
value="I got one more!"
/>
{/* You could use a Higher-Order Component! */}
<Route path="/other" component={hoc(Other, "I got other!")} />
{/* You could use a wrapper component! */}
<Route
path="/"
render={() => <Wrapper component={Home} value="I got home!" />}
/>
</Switch>
{/* You could have another switch for your footer (inline or within the component) */}
<Switch>
<Route
path="/another"
render={() => <Footer value="Switch footer another!" />}
/>
<Route
path="/other"
render={() => <Footer value="Switch footer other!" />}
/>
<Route
path="/onemore"
render={() => <Footer value="Switch footer onemore!" />}
/>
<Route path="/" render={() => <Footer value="Switch footer home!" />} />
</Switch>
</BrowserRouter>
);
Note the WrapperRoute would allow you to do validation on your match params before passing them through. You could do a Redirect if needed.
What I've ended up doing:
Since I'm using Redux, I added a piece of global state to keep track of the matched route.
And I dispatch actions to update that state from the render prop from the <Route/>'s component.
<Switch>
<Route key={index} exact path={"/some-route"} render={(routeProps) => {
// HERE I DISPATCH AN ACTION TO CHANGE THE STATE FOR THE CURRENT ROUTE
dispatch({
type: UPDATE_CURRENT_ROUTE,
payload: { name: "SOME_ROUTE_NAME" }
});
return (
<PrivacyPage
{...routeProps}
/>
);
}}/>
</Switch>
And now I can do on Footer.js:
function Footer() {
const currentRoute = useSelector((state) => state.currentRoute);
// RENDER FOOTER ACCORDINGLY TO THE CURRENT ROUTE
}