useEffect infinite loop when trying to check if user is logged in - javascript

Im using auth0, and on when the app gets re rendered maybe on a page refresh, im trying to do a couple of things with auth0, 1st is check if the user is authenticated, and if he is save his token and user object in the mobx store else redirect him to the login page.
this is the code im currently using:
// appLoaded --> to check if the user is logged in (app cannot be used without a user)
const App: React.FC = () => {
const {
// isAuthenticated,
user,
getIdTokenClaims,
loginWithRedirect,
isAuthenticated
} = useAuth0();
const {
sideNavStore: { isOpen, toggleSideBar },
authStore: { setAppLoaded, setToken, setUser, appLoaded }
} = useStore();
// get the user from auth0 and store their details in the mobx store;
useEffect(() => {
if (isAuthenticated) {
getIdTokenClaims().then((response) => {
// eslint-disable-next-line no-underscore-dangle
setToken(response.__raw);
setUser(user);
});
} else {
loginWithRedirect().then(() => {
setAppLoaded(false);
});
}
}, [
isAuthenticated,
setToken,
setUser,
setAppLoaded,
loginWithRedirect,
getIdTokenClaims,
user
]);
if (!appLoaded) return <PreLoader />;
return (
// some component code
)}
i run into an infinite loop in the code above. i have been on this issue for a day now and just decided to ask on here. could anyone please tell me what im doing wrong? thanks

You shouldn't update any state that a useEffect depends on from within it. That creates an infinite loop.
You have too many dependencies for single useEffect and you're updating some of them. Split your code into multiple useEffect with smaller number of dependencies, ensuring that each of them aren't updating it's own dependency

Related

Redirect in React router dom not redirecting to the given path

I have a make payment function which is called on a button click and if the user is not signed in i want to redirect that user to the signin component .
This is my code of the function.
const PaymentMethod = ()=>{
if(!isAuthenticated())
{
toast.error('Please signin to continue');
history.push('/signin') //works properly
// return <Redirect to = '/signin' /> // not working properly
}
}
// isAuthenticated is a boolean function that look whether the user is signed in or not
Try
return <Redirect to = '/signin' />
Because you have to render the Redirect to make an effect
If PaymentMethod a React component, then the history.push needs to be called in a useEffect hook with proper dependency or in a callback function as an intentional side-effect, not as an unintentional side-effect in the render body. The Redirect component would need to be returned from the component in order to have any effect.
useEffect Example to issue imperative redirect:
const PaymentMethod = () => {
const history = useHistory();
React.useEffect(() => {
if (!isAuthenticated()) {
toast.error('Please sign in to continue');
history.replace('/signin');
}
}, [history, toast]);
...
};
<Redirect> Example to issue declarative redirect:
const PaymentMethod = () => {
React.useEffect(() => {
if (!isAuthenticated()) {
toast.error('Please sign in to continue'); // <-- still a side-effect!!
}
}, [toast]);
if (!isAuthenticated()) {
return <Redirect to='/signin' />;
}
... regular JSX return ...
};
If PaymentMethod is the callback function then it must issue the imperative redirect via the history.replace function, returning JSX from an asynchronously called callback doesn't render it to the DOM. You should also not name the callback handler like it's a React function component. This is to remove any confusion.
const history = useHistory();
...
const paymentMethodHandler = () => {
if (!isAuthenticated()) {
toast.error('Please sign in to continue');
history.replace('/signin');
}
};
...
It looks like <Redirect to = '/signin' /> is a component and not a JS that can be called. Components need to be rendered in order for their JS to run.
If you for some reason want to specifically use redirect component you will have to put it into DOM on click.
you can simply use useNavigate() hook.
example
const PaymentMethod = () =>{
if(!isAuthenticated())
{
toast.error('Please signin to continue');
history.push('/signin')
useNavigate('/signin')
}
}

Why's the button displayed only when refreshing the page?

I want the logout button to show up only when the user's logged in. It kind of works now but it's not working the way it should.
With the code below, the logout button shows up when the user's logged in only if the page's refreshed. That's because upon the <App/> component being loaded, the <Navbar/> component mounted along with it.
But how can I make it so that even if <Navbar/>'s loaded, it can still be possible to manipulate when the button can appear based on if the auth token is not null?
Here's App.js:
const App = () => {
let [logoutButtonFlag, setLogoutButtonFlag] = useState(false);
let authToken = localStorage.getItem('token');
useEffect(() => {
if (authToken !== null) {
setLogoutButtonFlag(true);
}
}, [authToken]);
return (
<>
<Navbar logoutButtonFlag={logoutButtonFlag}/>
</>
);
}
export default App;
Here's Navbar.js:
const Navbar = (props) => {
return (
{!props.logoutButtonFlag ? null : <button className="learn-more">Logout</button>}
);
};
export default Navbar;
you are providing a non-state variable to the list of states that useEffect hook 'listen' to, so it will not run again after you change its value.
const [authToken, setAuthToken] = useState(localStorage.getItem('token'));
and when you update the local storge "token" also update authToken to the same value.
and your useEffect will retrigger on authToken change because now its a state
useEffect(() => {
if (authToken !== null) {
setLogoutButtonFlag(true);
}
}, [authToken]);
The reason way only on refresh it was updating is because the value of the "token" in local storage was changed already.

Return component message for 5 seconds and then redirect

I am wanting to implement the case where if I navigate to my URL with incorrect query parameters, a message is displayed and then I redirect to another page.
Imagine trying to navigate to a page only a logged in user can see when you're not logged in. I want it to render something like, 'You need to be logged in to see this content' and then after 2 - 5 seconds the page redirects to the /login page.
Note: Some of the code included is just pseudo-code.
I know that I can display either the logged in page or redirect with a simple Ternary
return hasQueryParams ? <MyLoggedInPage /> : <Redirect to={`/login`} />
however, I can't seem to get a setTimeout working to delay the redirect...
const redirect = () => {
let redirect = false;
setTimeout(() => {
redirect = true;
}, 5000);
return redirect
? <Redirect to={`/login`} />
: <h1>Need to be logged in for that</h1>;
}
return redirect();
For this, I am getting an error: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
I also tried using useState:
const [redirectNow, setRedirectNow] = useState(false);
useEffect(() => {
// Some code unrelated to the timeout/redirect
}, []);
const redirect = () => {
setTimeout(() => {
setRedirectNow(false);
}, 5000);
return redirectNow
? <Redirect to={`/login`} />
: <h1>Need to be logged in for that</h1>;
}
return redirect();
but this also gets a different error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3
I understand from reading further that we cannot access the useState stuff from inside an event handler.
Update
I should also add that I am already using useEffect for other things at this point.
Use a state to keep the track whether to redirect or not.
Update the state after the interval.
Redirect upon updating the state.
function RedirectComponent() {
const [shouldRedirect, setShouldRedirect] = React.useState(false);
React.useEffect(() => {
const id = setTimeout(() => {
setShouldRedirect(true);
}, 5000);
return () => clearTimeout(id);
}, []);
if (shouldRedirect) return <Redirect to="/login" />
return <h1>This is the message</h1>
}
I prefer to use custom hook.
In this case, useSetTimeout would work.
import React, { useState, useEffect, useRef } from 'react';
function useSetTimeout(timeoutCallback, seconds) {
const timeoutId = useRef();
useEffect(() => {
timeoutId.current = setTimeout(timeoutCallback, seconds);
return () => clearTimeout(timeoutId.current);
}, [])
}
function YourComponent(){
const [redirectNow, setRedirectNow] = useState(false);
useSetTimeout(()=>{
setRedirectNow(true);
}, 5000);
return redirectNow ? <Redirect to={`/login`} /> : <h1>Need to be logged in for that</h1>;
}

How can i intercept a component to check for permission

I have lots of static forms which i show the user when he clicks on the main menu and goes to a specific route, what i want to do now is to check when going to a route if that component has permission to be visited, i can do this by doing a simple post to server but i am confused and i don't know where should be the place to do this check or post.
Here are some of the solutions i thought of:
1- Writing a Higher order component and wrapping each static component with it
2- creating a base class and making each static form to inherit it while doing this check in the parent class
3- Or maybe using the routes as a solution since i am using the react-router ?
I will appreciate any help or tips.
Thanks.
Create a custom hook like so:-
const useAdmin = (url:string) => {
const [admin, setAdmin] = React.useState(false);
React.useEffect(() => {
post(url, {some: body}).then(res => {
setAdmin(res.admin);
}).catch(err => {
setAdmin(false);
});
}, [])
return [admin];
}
Then use it anywhere:-
const mycomponent = props => {
const [admin] = useAdmin('https://foobar.com/checkPermission');
//Rest of the logic according to the variable 'admin'
return (
<div>
{
admin? <div/>:null
}
</div>
)
}
Or think of admin as permission. Pass it some url for different routes and it will handle it.
I do something similar using react-router as well. I made my own route component that wraps around react-router's route that checks permissions and conditionally renders the route or redirects.
If you're doing the api call each time, then it would look something close to this.
class AppRoute extends Component {
state = {
validCredentials: null
}
componentDidMount() {
// api call here + response
if (ok) {
this.setState({validCredentials: true})
} else {
this.setState({ validCredentials: false})
}
}
render() {
const { validCredentials } = this.state
if (validCredentials) {
return <Route {...this.props} />
} else if (validCredentials === false) {
return <Redirect to="somewhere"/>
}
return null
}
}
You can definitely accomplish this using a Higher Order Component. Just set a state for the user on login like "admin" or "basic_user." According to this state some buttons or forms are going to be available for the user to access. You can also save these access permissions to your backend and call it in the HOC whenever the user logs in.

How to set up firebase auth onAuthStateChanged() listener run when React app is loaded?

I'm trying set up the Firebase auth listener on my top level component App in order to provide the authUser object via React context to all other components.
I'm currently doing this:
function App() {
console.log('Rendering App...');
const [authUser,setAuthUser] = useState(null);
const [authWasListened,setAuthWasListened] = useState(false);
useEffect(()=>{
console.log('Running App useEffect...');
return firebase.auth().onAuthStateChanged(
(authUser) => {
console.log(authUser);
console.log(authUser.uid);
if(authUser) {
setAuthUser(authUser);
setAuthWasListened(true);
} else {
setAuthUser(null);
setAuthWasListened(true);
}
}
);
},[]);
return(
authWasListened ?
<Layout/>
: <div>Waiting for auth...</div>
);
}
But I'm getting the log output:
Rendering App...
Running App useEffect...
It seems that I'm setting up the listener but it doesn't run at first, therefore it's not getting the current auth state (which is null, since I don't even have a login form yet). It seems like it's waiting for a change to occur.
Shouldn't the authListener get the current authUser state at first, and then listen to changes? What am I doing wrong?
UPDATE
I've found out what I was doing wrong. The useEffect should return a function to clear the listener on unmount and not a function call, as I was trying to do above. So my listener was never being set up.
This is working as intended now:
useEffect(()=>{
console.log('Running App useEffect...');
const authListener = firebase.auth().onAuthStateChanged(
(authUser) => {
console.log(authUser);
console.log(authUser.uid);
if(authUser) {
setAuthUser(authUser);
setAuthWasListened(true);
} else {
setAuthUser(null);
setAuthWasListened(true);
}
}
);
return authListener; // THIS MUST BE A FUNCTION, AND NOT A FUNCTION CALL
},[]);
See https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1
Why did we return a function from our effect? This is the optional cleanup mechanism for effects. Every effect may return a function that cleans up after it. This lets us keep the logic for adding and removing subscriptions close to each other. They’re part of the same effect!
When exactly does React clean up an effect? React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time. We’ll discuss why this helps avoid bugs and how to opt out of this behavior in case it creates performance issues later below.
And see Set an authentication state observer and get user data
Try the following code.
function App() {
console.log('Rendering App...');
const [authUser,setAuthUser] = useState(null);
const [authWasListened,setAuthWasListened] = useState(false);
useEffect(()=>{
console.log('Running App useEffect...');
firebase.auth().onAuthStateChanged(
(authUser) => {
console.log(authUser);
console.log(authUser.uid);
if(authUser) {
setAuthUser(authUser);
setAuthWasListened(true);
} else {
setAuthUser(null);
setAuthWasListened(true);
}
}
);
},[]);
return(
authWasListened ?
<Layout/>
: <div>Waiting for auth...</div>
);
}
The listener is not immediate. The change in state might take some time from the moment the page is first loaded. If you want an immediate check to see if a user is signed in, you can use currentUser, but again, it might take some time for that to update, so the listener is the better choice.

Categories