I am using functional component which provides authentication to the specific routes such as /dashboard using server side authentication happening in useeffect of my app function.
Authentication is working fine and also when I click dashboard button I get directed to dashboard when I am logged in else redirected to home page.
The problem arises when I reload the /dashboard page . At that time what I observe is everything is re-rendered and before going through use effect it first passes from AuthenticatedRoute which doesn't give authentication because server side auth is happening in use effect and I am directly redirected to home page even when I am logged in.
App.js
const AuthenticatedRoute = ({ children, isAuthenticated , ...rest }) => {
return (
<Route
{...rest}
render={() =>
isAuthenticated ? (
<div>{children}</div>
) : (
<Redirect to="/home" />)}
></Route>
);
};
Route code:
App.js
<AuthenticatedRoute isAuthenticated = {isAuthenticated} path="/dashboard">
<AgentDashboard />
</AuthenticatedRoute>
App.js
function App() {
const [authTokenValid, setauthTokenValid] = useState(false)
useEffect(() => {
const token = localStorage.getItem('Authorization')
const authMainPageCheck = async () => {
await axios.post(tokenAuthCheckURL , {
'token':token,
}).then(result => result.data).then(
result => {
if(result.status === 200){
console.log("Authentication Given ")
setauthTokenValid(true)
}
else{
console.log("Authentication Not Given ")
setauthTokenValid(false)
}
})
}
authMainPageCheck()
}, [])
Please try this code below:
import React, { useEffect, useState } from "react";
import { Route, Redirect, BrowserRouter } from "react-router-dom";
// import axios from "axios";
// #ts-ignore
const AuthenticatedRoute = ({ children, isAuthenticated, ...rest }) => {
return (
<Route
{...rest}
render={() =>
isAuthenticated ? (
<div>
{children}
</div>
) : (<Redirect to="/error" />)
}
></Route>
);
};
export const App = () => {
// Set initial value to null
const [authTokenValid, setauthTokenValid] = useState(null)
useEffect(() => {
// Wait for request to complete
// Example...
setTimeout(() => {
// #ts-ignore
setauthTokenValid(true);
}, 3000);
// const token = localStorage.getItem('Authorization');
// const authMainPageCheck = async () => {
// await axios.post(tokenAuthCheckURL, {
// token,
// }).then(result => result.data).then(
// result => {
// if (result.status === 200) {
// console.log("Authentication Given ")
// setauthTokenValid(true)
// } else {
// console.log("Authentication Not Given ")
// setauthTokenValid(false)
// }
// }
// )
// }
}, []);
if (authTokenValid === null)
// Wait Until a Non Null Response Comes....
return (<h1>Loading...</h1>); // Create a new loading component...
else
return (
<BrowserRouter>
<AuthenticatedRoute isAuthenticated={authTokenValid} exact path="/">
<h1>This is Authenticated Home!!!</h1>
</AuthenticatedRoute>
<AuthenticatedRoute isAuthenticated={authTokenValid} exact path="/dashboard">
<h1>This is Authenticated Dashboard!!!</h1>
</AuthenticatedRoute>
</BrowserRouter>
);
}
Related
I am trying to create protected routing and have created a component called Protected which fetches the access token from the backend if user exists and displays the protected components if user exists or gets navigated to the Not logged in component. This functionality is not working as expected. For some reason I am getting current user as undefined. Is it because the protected route's Navigate is getting rendered even before access token gets generated and stored in localStorage?
Below is the code
import React, { useEffect, useState } from 'react'
import { Outlet, Navigate } from 'react-router-dom'
import authService from '../services/auth.service';
function Protected() {
const [currentUser, setCurrentUser] = useState(undefined);
console.log(currentUser);
useEffect(() => {
const user = authService.getCurrentUser();
if (user) {
setCurrentUser(user);
}
}, []);
return (
currentUser ? <Outlet/> : <Navigate to='/notloggedin'/>
)
}
export default Protected
authService.js
import axios from "axios";
const API_URL = "http://localhost:3000/auth";
const signup = (email, password) => {
try {
return axios
.post(API_URL + "/signup", {
email,
password,
})
.then((response) => {
//console.log(response.data.errors);
if (response.data.accessToken) {
localStorage.setItem("user", JSON.stringify(response.data));
}
console.log(response.data.errors?.map((error) => console.log(error.msg)))
return response.data;
});
} catch(error) {
return error;
}
};
const login = (email, password) => {
return axios
.post(API_URL + "/login", {
email,
password,
})
.then((response) => {
if (response.data.accessToken) {
localStorage.setItem("user", JSON.stringify(response.data));
}
console.log(response.data.errors?.map((error) => console.log(error.msg)))
return response.data;
});
};
const logout = () => {
localStorage.removeItem("user");
};
const getCurrentUser = () => {
return JSON.parse(localStorage.getItem("user"));
};
const authService = {
signup,
login,
logout,
getCurrentUser,
};
export default authService;
App.js
function App() {
return (
<>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/notloggedin" element={<NotLoggedIn/>} />
<Route element={<Protected/>}>
<Route path="/restaurants" element={<Food_Main />} />
<Route path="/:food/:id" element={<Food_Detail />} />
<Route path="/payment" element={<PaymentDetails />} />
<Route path="/thankyou" element={<ThankYou />} />
</Route>
</Routes>
</>
);
}
export default App;
The initial currentUser value is undefined which is also a falsey value and the useEffect hook runs at the end of the render cycle, so on the initial render the Navigate component is returned and the user is navigated off the route you are trying to protect.
Render null or some loading indicator while the auth status is being validated/verified.
Example:
function Protected() {
const [currentUser, setCurrentUser] = useState(undefined);
useEffect(() => {
const user = authService.getCurrentUser();
setCurrentUser(!!user); // <-- sets to defined boolean value
}, []);
if (currentUser === undefined) {
return null; // or loading indicator/spinner/etc...
return currentUser
? <Outlet/>
: <Navigate to='/notloggedin' replace />;
}
I have used react-router-dom for Navigation..but My Problem is without authentication also Dashboard Screen is being visible for mili seconds.
App.js
<Route index path="/" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/Login" element={<Login />} />
ProtectedRoute
const ProtectedRoute = ({ children }) => {
const { user } = useMyContext();
if (!user) {
return <Navigate to="/Login" />;
}
return children;
};
export default ProtectedRoute;
Login.js
onClick..
await login(data.get('email'), data.get('password'));
navigate('/', { replace: true })
Context.js
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password)
}
function logOut() {
return signOut(auth);
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentuser) => {
setUser(currentuser);
});
return () => {
unsubscribe();
}
}, [])
How can I protect my protected screen from unauthorized access?
The issue is that your ProtectedRoute component doesn't wait for the authentication status to be confirmed. In other words, the default user state masks one of either the authenticated or unauthenticated status.
It should conditionally render a loading indicator while onAuthStateChanged is making the first call to determine the user's authentication status. For the initial user state value use a value that is neither a defined user object in the case of an authenticated user or null in the case of an unauthenticated user. undefined is a good initial value.
Example:
Context
const [user, setUser] = React.useState(); // initially undefined
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password);
}
function logOut() {
return signOut(auth);
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentuser) => {
setUser(currentuser);
});
return unsubscribe;
}, []);
ProtectedRoute
const ProtectedRoute = ({ children }) => {
const { user } = useMyContext();
if (user === undefined) {
return null; // or loading indicator/spinner/etc
}
return user ? children : <Navigate to="/Login" replace />;
};
I am doing a React.js project. I am retrieving dat from the Star Wars API rendering a list of films on the screen and now I am trying to route every film to its own page through react-router-dom. Unfortunately, I am not able to make it work. it crash when I try to routing dynamically.
UPDATE AFTER ANSWER OF REZA
This is the App.js:
import './App.css';
import { Route, Routes } from "react-router-dom";
import Home from './components/Home';
import ItemContainer from './components/ItemContainer';
import Navbar from './components/Navbar';
function App() {
return (
<>
<Navbar />
<Routes>
<Route exact path='/' element={<Home />} />
<Route exact path="/:movieId" element={<ItemContainer />} />
</Routes>
</>
);
}
export default App;
This is the ItemContainer:
import { useEffect, useState } from "react";
import MovieDetail from "../MovieDetail";
import { useParams } from "react-router-dom";
const ShowMovie = (movieId) => {
const [result, setResult] = useState([]);
const fetchData = async () => {
const res = await fetch("https://www.swapi.tech/api/films/");
const json = await res.json();
setResult(json.result);
}
useEffect(() => {
fetchData();
}, []);
return new Promise((res) =>
res(result.find((value) => value.properties.title === movieId)))
}
const ItemContainer = () => {
const [films, setFilms] = useState([]);
const { movieId } = useParams([]);
console.log('params movieId container', movieId)
useEffect(() => {
ShowMovie(movieId).then((value) => {
setFilms(value.properties.title)
})
}, [movieId])
return (
<MovieDetail key={films.properties.title} films={films} />
);
}
export default ItemContainer;
The console.log doesn't give anything.
UPDATE
Also, this is the whole code in sandbox.
ShowMovie is declared like a React component, but used like a utility function. You shouldn't directly invoke React function components. React functions are also to be synchronous, pure functions. ShowMovie is returning a Promise with makes it an asynchronous function.
Convert ShowMovie into a utility function, which will basically call fetch and process the JSON response.
import { useEffect, useState } from "react";
import MovieDetail from "../MovieDetail";
import { useParams } from "react-router-dom";
const showMovie = async (movieId) => {
const res = await fetch("https://www.swapi.tech/api/films/");
const json = await res.json();
const movie = json.result.find((value) => value.properties.episode_id === Number(movieId)));
if (!movie) {
throw new Error("No match found.");
}
return movie;
}
const ItemContainer = () => {
const [films, setFilms] = useState({});
const { movieId } = useParams();
useEffect(() => {
console.log('params movieId container', movieId);
showMovie(movieId)
.then((movie) => {
setFilms(movie);
})
.catch(error => {
// handle error/log it/show message/ignore/etc...
setFilms({}); // maintain state invariant of object
});
}, [movieId]);
return (
<MovieDetail key={films.properties?.title} films={films} />
);
};
export default ItemContainer;
Update
The route path should include movieId, the param you are accessing in ItemContainer. The sub-path "film" should match what you link from in Home. In Home ensure you link to the /"film/...." path.
<Routes>
<Route path="/films" element={<Home />} />
<Route path="/film/:movieId" element={<ItemContainer />} />
<Route path="/" element={<Navigate replace to="/films" />} />
</Routes>
In ItemContainer you should be matching a movie object's episode_id property to the movieId value. Store the entire movie object into state, not just the title.
const showMovie = async (movieId) => {
const res = await fetch("https://www.swapi.tech/api/films/");
const json = await res.json();
const movie = json.result.find((value) => value.properties.episode_id === Number(movieId)));
if (!movie) {
throw new Error("No match found.");
}
return movie;
}
...
useEffect(() => {
console.log("params movieId container", movieId);
showMovie(movieId)
.then((movie) => {
setFilms(movie);
})
.catch((error) => {
// handle error/log it/show message/ignore/etc...
setFilms({}); // maintain state invariant of object
});
}, [movieId]);
You should also use Optional Chaining on the more deeply nested films prop object properties in MovieDetail, or conditionally render MovieDetail only if the films state has something to display.
const MovieDetail = ({ films }) => {
return (
<div>
<h1>{films.properties?.title}</h1>
<h3>{films.description}</h3>
</div>
);
};
Demo:
Modify App.js like this:
function App() {
return (
<>
<Navbar />
<Routes>
<Route exact path="/" element={<Home />} />
<Route exact path="/MovieDetail/:movieId" element={<ItemContainer />} />
</Routes>
</>
);
}
I use the axios post to request to the back-end if the user have access to the application. The problem is the axios returns undefined and then true or false . Have a private Route to manage what to do in case returns true or false (in this case undefined = false) ,is axios the problem or is there some other way? like wait until returns true or false
IsLogin.jsx
import React from 'react'
const axios = require('axios');
export const AuthContext = React.createContext({})
export default function Islogin({ children }) {
const isAuthenticated =()=>{
try{
axios.post('/api/auth').then(response => {
var res = response.data.result;
console.log(res)
return res
})
} catch (error) {
console.error(error);
return false
}
}
var auth = isAuthenticated()
console.log(auth);
return (
<AuthContext.Provider value={{auth}}>
{children}
</AuthContext.Provider>
)
}
privateRoute.js
import React, { useContext } from 'react';
import { Route, Redirect } from 'react-router-dom';
import {AuthContext} from '../utils/IsLogin';
const PrivateRoute = ({component: Component, ...rest}) => {
const {isAuthenticated} = useContext(AuthContext)
return (
// Show the component only when the user is logged in
// Otherwise, redirect the user to /unauth page
<Route {...rest} render={props => (
isAuthenticated ?
<Component {...props} />
: <Redirect to="/unauth" />
)} />
);
};
export default PrivateRoute;
app.js
class App extends Component {
render() {
return (
<>
<BrowserRouter>
<Islogin>
<Header/>
<Banner/>
<Switch>
<PrivateRoute exact path="/index" component={Landing} />
<PrivateRoute path="/upload" component={Upload} exact />
<PublicRoute restricted={false} path="/unauth" component={Unauthorized} exact />
</Switch>
</Islogin>
</BrowserRouter>
</>
);
}
}
You don't want to return anything in your post request. You should be updating your context store
const isAuthenticated = () => {
try {
axios.post('/api/auth').then(response => {
var res = response.data.result;
console.log(res)
// update your context here instead of returning
return res
})
} catch (error) {
console.error(error);
return false
}
}
In your private route, have a componentDidUpdate style useEffect hook to check for changes in authentication status and update an internal flag on an as-needed basis
const PrivateRoute = ({ component: Component, ...rest }) => {
const { isAuthenticated } = useContext(AuthContext)
const [validCredentials, setValidCredentials] = React.useState(false)
React.useEffect(() => {
if (typeof isAuthenticated === 'boolean') {
setValidCredentials(isAuthenticated)
}
}, [isAuthenticated])
return (
// Show the component only when the user is logged in
// Otherwise, redirect the user to /unauth page
<Route {...rest} render={props => (
validCredentials ?
<Component {...props} />
: <Redirect to="/unauth" />
)} />
);
};
I am curious as to why you didn't use 'async await',lol.
You are making a post request to the endpoint '/api/auth',but you didn't give it any data to post,like:
try{
axios.post('/api/auth',{username,password}).then(response => {
var res = response.data.result;
console.log(res)
return res
})
} catch (error) {
console.error(error);
return false
}
The idea is that I've got a component that renders something but in the meantime is checking something that will return or redirect to another component:
useEffect(() => {
(() => {
if (true) {
// return to one component
}
// return to another component
})();
});
return (
<div> Javier </div>
);
I think that it is possible using the useEffect hook, but the problem is that, it does not redirect to my components, I tried using Redirect from the react-router, returning the component itself, and also using the history package, in this case, only replaced the url but no redirection at all.
Is this possible? Or maybe I'm way off the point.
Thanks a lot!
if you are just needing conditional rendering you could do something like this:
const LoadingComponent = () => <div> Javier </div>
function Landing(props) {
const [state={notLoaded:true}, setState] = useState(null);
useEffect(() => {
const asyncCallback = async () =>{
const data = await axios.get('/someApiUrl')
setState(data)
}
asyncCallback()
},[]);
if(!state){
return <FalseyComponent />
}
if(state.notLoaded){
//return some loading component(s) (or nothing to avoid flicker)
return <LoadingComponent /> // -or- return <div/>
}
return <TruthyComponent />
}
or redirect completely:
const LoadingComponent = () => <div> Javier </div>
function Landing(props) {
const [state={notLoaded:true}, setState] = useState(null);
useEffect(() => {
const asyncCallback = async () =>{
const data = await axios.get('/someApiUrl')
setState(data)
}
asyncCallback()
},[]);
if(!state){
return <Redirect to='/falseyRoute' />
}
if(state.notLoaded){
//return some loading component(s) or (nothing to avoid flicker)
return <LoadingComponent /> // -or- return <div/>
}
return <Redirect to='/truthyRoute' />
}
Using React router v6 you can create a redirection using useEffect:
import React, { useEffect } from 'react';
import {
BrowserRouter, Route, Routes, useNavigate,
} from 'react-router-dom';
const App = () => (
<div>
<BrowserRouter>
<Routes>
<Route path="/" element={<Main />} />
<Route path="/home" element={<Home />} />
</Routes>
</BrowserRouter>
</div>
);
const Main = () => {
const navigate = useNavigate();
useEffect(() => {
let didCancel = false;
const goToHomePage = () => navigate('/home');
if (!didCancel) { goToHomePage(); }
return () => { didCancel = true; };
}, [navigate]);
return (
<div>
<h1>Welcome Main!</h1>
</div>
);
};
const Home = () => (
<div>
<h1>Welcome Home!</h1>
</div>
);
export default App;
If you want to create an alternative redirection to another component, you can do it as below:
import React, { useEffect } from 'react';
import {
BrowserRouter, Route, Routes, useNavigate,
} from 'react-router-dom';
const App = () => (
<div>
<BrowserRouter>
<Routes>
<Route path="/" element={<Main />} />
<Route path="/home" element={<Home />} />
<Route path="/other" element={<Other />} />
</Routes>
</BrowserRouter>
</div>
);
const Main = () => {
const navigate = useNavigate();
useEffect(() => {
let didCancel = false;
const goToHomePage = () => navigate('/home');
const goToOtherPage = () => navigate('/other');
if (!didCancel) { goToHomePage(); } else { goToOtherPage(); }
return () => { didCancel = true; };
}, [navigate]);
return (
<div>
<h1>Welcome Main!</h1>
</div>
);
};
const Home = () => (
<div>
<h1>Welcome Home!</h1>
</div>
);
const Other = () => (
<div>
<h1>Welcome Other!</h1>
</div>
);
export default App;
In React router 5 with changed old syntax it should also work. However, in React router 6 I did not find Redirect so the above redirection is more useful.
Try to return based on some state value like this.
import { Redirect } from "react-router-dom"; //import Redirect first
const [redirctTo, setRedirctTo] = useState(false); // your state value to manipulate
useEffect(() => {
(() => {
if (true) {
setRedirctTo(true)
}
// return to another component
})();
});
if(redirctTo){
return <Redirect to="/your-url" />
} else {
return (
<div> Javier </div>
);
}