Trying to make authenticated route using react hooks, with the snippet below, it appears when i refreshed the page while the token is available it doesn't get picked up in the useEffect at the first render. What i'm doing wrong here
const AuthRoute = ({ component: Component, ...rest }) => {
const context = useContext(AuthStateContext)
const token = window.localStorage.getItem('token')
useEffect(() => {
if (token) {
context.setLogin()
}
console.log("TOKEN ->", token)
}, [])
return (
<Route
{...rest}
render={props => (
context.isAuthenticated ?
<Component {...props} />
: <Redirect to={{
pathname: "/",
state: { from: props.location }
}} />
)}
/>
)
}
I assume context.setLogin() will set context.isAuthenticated to true. If that's the case, then it explains.
Callback function of useEffect(callback) is always called asynchronously after each render. As of first render, setLogin() is only called after value of isAuthenticated is read, which at the time of accessing should be false.
So render logic goes to the second branch <Redirect /> which immediately takes user to other place.
To achieve this, you can defer the rendering util state of isAuthenticated is decided.
(Here, I assume you want to check and set isAuthenticated once early up in the rendering tree, then broadcast the isAuthenticated through AuthStateContext object for other parts of the app to notice.)
I suggest this pattern:
const AuthRoute = ({ component: Component, ...rest }) => {
const context = useContext(AuthStateContext)
const [authChecked, setAuthChecked] = useState(false)
useEffect(() => {
const token = window.localStorage.getItem('token')
if (token) context.setLogin()
setAuthChecked(true)
console.log("TOKEN ->", token)
}, [])
if (!authChecked) return null // <-- the trick!
return (
<Route
{...rest}
render={props => (
context.isAuthenticated ?
<Component {...props} />
: <Redirect to={{
pathname: "/",
state: { from: props.location }
}} />
)}
/>
)
}
Side note, if context.setLogin() only asynchronously sets the state of isAuthenticated, then you need to add in a callback or promise pattern like
context.setLogin(() => setAuthChecked(true))
// or
context.setLogin().then(() => setAuthChecked(true))
Related
I have a react-app with authentication (cookie based) with a login route and a profile route. The profile route is fetching the profile data and puts the data in a form. One of the fields is a language field. I'm using the useEffect hook to watch the language property and use i18n.changeLanguage() to change the language.
For some reason the page keeps refreshing when I add this code. It must be a combination of this code together with the code I'm using the check if the user is authenticated to access the route. When I comment out the protectedRoute function or the useEffect hook it's working but I obviously need both.
A small breakdown of the protectedRoute function and authContext.
The routes are wrapped in an AuthProvider
const App = () => {
return (
<BrowserRouter>
<AuthProvider>
<Router />
</AuthProvider>
</BrowserRouter>
);
};
Inside the AuthProvider I have a user and isAuthenticated state. Both starting with a value of null. On mount a call with or without a cookie is done to the backend to get the user info. If a user object is returned with an id the isAuthenticated state is set to true.
const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(null);
const [user, setUser] = useState(null);
useEffect(() => {
getUserInfo();
}, []);
const getUserInfo = async () => {
try {
const { data } = await authService.me();
setIsAuthenticated(true);
setUser(data);
} catch (error) {
setIsAuthenticated(false);
setUser({});
}
};
const setAuthInfo = (user) => {
setIsAuthenticated(!!(user && user.id));
setUser(user);
};
...
As long as isAuthenticated is null a loading state is rendered instead of a route.
if (authContext.isAuthenticated === null) {
return (
<div>
<span>Loading...</span>
</div>
);
}
const ProtectedRoutes = () => {
return authContext.isAuthenticated ? (
<Outlet />
) : (
<Navigate to="/login" replace />
);
};
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<ProtectedRoutes />}>
<Route path="/" element={<Navigate to="/profile" replace />} />
<Route path="/profile" element={<ProfileOverview />} />
</Route>
</Routes>
);
The profile page can be accessed when the isAuthenticated state is true. Inside the profile page the user profile information is fetched and with a reset set into the form state. This will trigger the useEffect hook watching the formData.language property which will set the language to the user's language. This leads to a continuous refresh and I can't find the reason why or what I'm doing wrong.
const Profile = () => {
const { i18n } = useTranslation();
const { formData, reset, handleSubmit, handleChange } = useForm({});
useEffect(() => {
console.log("Get profile data");
getProfileInfo();
}, []);
useEffect(() => {
i18n.changeLanguage(formData.language);
}, [formData.language]);
const getProfileInfo = async () => {
const { data } = await profileService.getProfileInfo();
reset(data);
};
const submit = (values) => {
console.log("submit");
};
...
Codesandbox demo over here. I have put a console.log inside the useEffect on the profile page so you can see that it keeps refreshing. Login can be done without credentials. All fetches are done with a setTimeout to fake real calls.
Taken from the Codesandbox demo and modified within Profile.js. The idea is to block additional requests to i18n.changeLanguage until the previous request has finished and the language was truly updated.
// Use a state variable to verify we aren't already loading
const [isLoadingLanguage, setLoadingLanguage] = useState(false);
useEffect(() => {
// Verify we aren't loading and are actually changing the language
if (!isLoadingLanguage && formData.language !== language) {
const load = async () => {
// Set the state as loading so we don't perform additional requests
setLoadingLanguage(true);
// Since this method returns a promise, and useEffect does not allow async/await easily,
// make and call an async method so we can await changeLanguage
await i18n.changeLanguage(formData.language);
// Originally, we wanted to set this again to update when the formData language updated, but we'll do in submit
// setLoadingLanguage(false);
};
load();
}
}, [i18n, formData.language]);
I would note, getting this to work in the sandbox required removing the line to i18n.changeLanguage so perpetual renders would not keep submitting requests. Then, adding back the useEffect above loaded the form and provided single submissions.
Edit: To prevent i18n.changeLanguage from rendering the component again we can do a few things.
Remove the setLoadingLanguage(false) from the useEffect so we don't trigger a language change until we submit
Add i18n.changeLanguage to the submit method AND localStorage.setItem("formData", JSON.stringify(formData) to retain the state when we do submit
const submit = (values) => {
localStorage.setItem("formData", JSON.stringify(formData));
i18n.changeLanguage(formData.language);
console.log("submit");
};
Retrieve any localStorage item for "formData" in place of the getProfileInfo
export default {
getProfileInfo() {
console.log({ localStorage });
const profileData =
localStorage.getItem("formData") !== null
? JSON.parse(localStorage.getItem("formData"))
: {
first_name: "John",
last_name: "Doe",
language: "nl"
};
const response = {
data: profileData
};
return new Promise((resolve) => {
setTimeout(() => resolve(response), 100);
});
}
};
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>
);
}
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
}
I have a path /player/:id/
If I navigate from say /player/12 to /player/13 it will error out, for example: cannot find foo of undefined. However if I navigate from a different path such as /heroes/100 to /player/12 it will load fine.
My suspicion is that when I link from the same route, but different param, the new page is not invoking my useEffect or setState hooks.
I feel like I'm missing something fundamental here, but I can't figure it out. If my suspicion is correct, does anyone know how i can force my component to use my UseEffect and UseState hooks before loading the page when going from the same route, but different params?
The params are correct, it's displaying the correct id in the url bar. If i refresh the page it'll load fine, but if i click the link it'll error out Cannot read property 'hero_id' of undefined
Thank you
Context:
playedAgainst.map(player => <Link key={Math.random()} to=
{"/players/" + props.invertedPlayers[player]}> <span> {player} </span></Link>
Each iteration creates a new Player name, with a Link. When i click on that link it says all "cannot read property hero_id of undefined". However, when i refresh the page it loads fine. I'm 100% sure the link path is correct. I used the exact same code in a different component that leads to the Player component.
(The Math.random is definitely sloppy, i'll remove that!)
<Header />
<Switch>
<Route exact path="/">
<Search players={this.state.players} />
<GameList players={this.state.players} data={this.state.games} />
<Footer />
</Route>
<Route exact path="/players/:id">
<Player players={this.state.players} invertedPlayers = {_.invert(this.state.players)} />
</Route>
<Route exact path="/heroes/:id">
<Heroes players={this.state.players} invertedPlayers = {_.invert(this.state.players)} />
</Route>
</Switch>
</Router>
EDIT -
I added the following hook to my Players component.
useEffect(() => {
console.log('useEffect fired');
}, [])
When i go from 1 id to another (same route, different param) the useEffect hook is not firing.
Seems like i need to get my useEffect to fire when following the same routes with different ids.
Accessing my Params via useParams():
let { id } = useParams();
const [player, setPlayerData] = useState({ data: null, image: null });
useEffect(() => {
const fetchData = async () => {
const response1 = await axios(`/api/players/${id}`);
const response2 = await axios(`/api/saveImage/${id}`)
setPlayerData({ data: response1.data, image: response2.data });
};
fetchData();
}, []);
EDIT 2 -
Added to ID to useEffect, but useEffect is still not rerunning for some reason:
let { id } = useParams();
const [player, setPlayerData] = useState({ data: null, image: null });
useEffect(() => {
const fetchData = async () => {
const response1 = await axios(`/api/players/${id}`);
const response2 = await axios(`/api/saveImage/${id}`)
setPlayerData({ data: response1.data, image: response2.data });
};
fetchData();
}, [id]);
EDIT 3 -
More context, hopefully this helps:
As soon as the component loads it pulls data from the API to load within the component. This works fine. When i click a ling to the same route, but different param, the useEffect hook to pull data doesn't run.
const Player = (props) => {
let { id } = useParams();
const [player, setPlayerData] = useState({ data: null, image: null });
useEffect(() => {
const fetchData = async () => {
const response1 = await axios(`/api/players/${id}`);
const response2 = await axios(`/api/saveImage/${id}`)
setPlayerData({ data: response1.data, image: response2.data });
};
fetchData();
}, [id]);
useEffect(() => {
console.log('count changed');
}, [])
console.log('test')
return (
<div className="historyContainer">
...............
{player.data && player.data.map((game, i) => {
const heroIdOfPlayer = game.players.find(p => p.account_id === +id).hero_id;
const teamOfPro = game.players.find(p => p.hero_id === +heroIdOfPlayer).team;
The part under the '.......' is where Cannot read property 'hero_id' of undefined is coming from, which only happens when clicking through the link.
A few lines further down:
{playedAgainst ?
playedAgainst.map(player => <Link key={Math.random()} to={"/players/" + props.invertedPlayers[player]}><span> {player} </span></Link>)
: ""}
This is how each link is generated. The path is correct. if i follow the path it'll say Cannot read property 'hero_id' of undefined, but when i refresh the page it will load fine.
EDIT - 4
Adding my App component in case this helps:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
games: [],
players: {},
};
}
componentDidMount() {
ReactGA.pageview(window.location.pathname)
this.loadGames();
this.loadPlayers();
this.interval = setInterval(() => {
this.loadGames();
}, 5000);
}
componentWillUnmount() {
this.loadGames();
clearInterval(this.interval);
}
async loadGames() {
const response = await fetch('/api/games');
const myJson = await response.json();
const sortedJson = myJson.sort((a, b) =>
a.spectators > b.spectators ? -1 : 1,
);
this.setState({ games: sortedJson });
}
async loadPlayers() {
const response = await fetch('/api/playersList');
const myJson = await response.json();
this.setState({ players: myJson });
}
render() {
return (
<Router history={history}>
<Header />
<Switch>
<Route exact path="/">
<Search players={this.state.players} />
<GameList players={this.state.players} data={this.state.games} />
<Footer />
</Route>
<Route exact path="/players/:id">
<Player players={this.state.players} invertedPlayers = {_.invert(this.state.players)} />
</Route>
<Route exact path="/heroes/:id">
<Heroes players={this.state.players} invertedPlayers = {_.invert(this.state.players)} />
</Route>
</Switch>
</Router>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
In your useEffect hook, pass the prop that changes as a dependency i.e.
useEffect(..., [id])
This will ensure useEffect runs everytime id changes, the problem at the minute is you pass [] which means useEffect will only run once, when the component is first mounted.
I am trying to make protected routes with react and redux , i have made actions on redux for initaiting auth()!
Here's a route :
<BrowserRouter baseName="/">
<Switch>
<PublicRoute authenticated={this.props.isAuthenticated} exact path="/loginapp" component={LoginApp} />
<PrivateRoute authenticated={this.props.isAuthenticated} exact path="/dashboard" component={dashboardContainer} />
<PrivateRoute authenticated={this.props.isAuthenticated} exact path="/leaderboard" component={leaderBoardContainer} />
</Switch>
</BrowserRouter>
Here's the custom route:
export const PublicRoute = ({component: Component, authenticated, ...props}) => {
const { from } = props.location.state || { from: { pathname: "/dashboard" } };
return (
<Route
{...props}
render={(props) => authenticated === true
? <Redirect to={from} />
: <Component {...props} />}
/>
);
};
class PrivateRoute extends React.Component {
render() {
const { component: Component, authenticated, ...rest } = this.props;
const isLoggedIn = authenticated;
return (
<Route
{...rest}
render={props => isLoggedIn
? <Component {...props} />
: <Redirect to={{ pathname: "/loginapp", state: {from: props.location }}} />}
/>
);
}
};
I have got 3 routes , /loginapp, /dashboard, /leaderboard , in which , i wanted the user shouldn't view /loginapp route again if the user refreshes the page and automatically should be logged in with the data saved in redux . But when i refresh the page the data gets lost and IT STAYS IN /LOGINAPP route AND I HAVE TO MANUALLY LOGIN.
Here is my loginapp code:
import PropTypes from "prop-types";
import * as actions from "../store/actions";
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import classes from "./LoginApp.scss";
class LoginApp extends Component {
constructor(props) {
super(props);
}
pushme =() => {
console.log("==================");
this.props.initAuth();
this.props.authHandler();
}
loginFormHandler = () => (
<div>
<span>this is login page</span>
<button type="button" onClick={this.pushme}>get login</button>
</div>
);
render() {
return (
<div className={classes.LoginApp}>
{this.loginFormHandler()}
</div>
);
}
}
LoginApp.propTypes = {
initAuth: PropTypes.func,
authHandler: PropTypes.func,
isAuthenticated: PropTypes.bool,
location: PropTypes.object
};
const mapStateToProps = state => {
return {
isAuthenticated: state.auth.isAuthenticated
};
};
const mapDispatchToProps = dispatch => {
return {
initAuth: () => dispatch(actions.initAuth()),
authHandler: () => dispatch(actions.authSignIn())
};
};
export default connect(mapStateToProps,mapDispatchToProps)(LoginApp);
here is the action creator / and the reducer is already created by me:
export const authSignIn = () => async (dispatch) => {
const success = await auth.signInWithEmailAndPassword("iphonexs#apple.com", "12345678").catch((error) => console.error(error));
success.user.uid !== undefined ?
dispatch(authSuccess(success.user.displayName)) :
dispatch(authFailed("INVALID : FAILED TO LOG IN "));
};
Please have a look. i have been stuck .
i wanted the user shouldn't view /loginapp route again if the user refreshes the page and automatically should be logged in with the data saved in redux . But when i refresh the page the data gets lost and IT STAYS IN /LOGINAPP route AND I HAVE TO MANUALLY LOGIN.
That's the expected behavior. Redux is not a persistent data store, when you reload the page everything will be gone. To persist authentication status across reloads, you would need to save the state in sessionStorage or localStorage with regard to your needs.
Also the following question may be helpful for you.
How can I persist redux state tree on refresh?