React - State variable not being assigned as expected - reactjs

In my app, I have a custom protected route that renders content dynamically based on isAuthenticated variable shown below.
return (
<Route {...rest} render={() => {
return isAuthenticated === true ? children : <Redirect to='/login' />
}} />
)
The following code is above this:
const [isAuthenticated, setIsAuthenticated] = useState(null)
useEffect(() => {
const isAuth = async () => {
try {
const result = await axios.get(`${BASE_URL}/api/auth/isauth`, {headers: {'x-auth-token': localStorage.getItem("userJWT")}})
setIsAuthenticated(true)
}
catch (err) {
if (err) setIsAuthenticated(false)
}
}
isAuth()
}, [])
When the user goes to this protected route, the useState is set to null by default, however this causes a redirect even if the user is authenticated. How do I set the state initally to the result of the isAuth function (true or false) in the useEffect instead of a value of null? Thanks.

Related

How to run use effect in App.js while using react router dom

I want to run getUser function every time the user goes to some other link.
The following is my getUser function
const getUser = async () => {
if (localStorage.getItem('access') === null || localStorage.getItem('refresh') === null || localStorage.getItem('user') === null) {
setUser({ email: null });
setIsLoggedIn(false);
return;
}
const responseForAccessToken = await verifyTokenAPI(localStorage.getItem('access'));
console.log(responseForAccessToken);
if (responseForAccessToken.status >= 400) {
const newAccessTokenResponse = await getAccessTokenAPI(localStorage.getItem('refresh'));
console.log(newAccessTokenResponse);
if (newAccessTokenResponse.status >= 400) {
localStorage.removeItem('access');
localStorage.removeItem('refresh');
localStorage.removeItem('user');
setUser({ email: null });
setIsLoggedIn(false);
return;
}
localStorage.removeItem('access');
localStorage.setItem('access', newAccessTokenResponse.access);
}
I want to verify token every time the user changes routes. Therefore, I used getUser function in useEffect in my App.js
const history = useHistory();
const { getUser } = useAuth();
useEffect(() => {
history.listen((location) => {
console.log(`You changed the page to: ${location.pathname}`);
});
getUser();
}, [history]);
Every time I change routes the useEffect runs and console logs the message but does not run getUser function.
I am using Link from react-router-dom
<h1>Welcome {user.email}</h1>
<Link to="/protected-route-2">Protected Route 2</Link>
<button
onClick={() => logout({ callBack: () => history.push("/login") })}
>
Logout
</button>
Additionally, I also have a PrivateRoute component
const Privateroute = ({ component: Component, ...rest }) => {
const { isLoggedIn, getUser } = useAuth()
console.log(isLoggedIn);
const location = useLocation()
if (isLoggedIn) return <Route {...rest} render={props => <Component {...props} />} />;
return <Redirect to={{ pathname: '/login', state: { from: location.pathname } }} />
}
I am not sure if I am doing things right. Can someone suggest another approach to this problem? Any suggestion will be appreciated. Thanks in advance.
You should use the useLocation hook (as shown in the documentation) instead of the useHistory, which would give you the current location and use that as the dependency for the useEffect:
const location = useLocation();
const { getUser } = useAuth();
useEffect(() => {
console.log(`You changed the page to: ${location.pathname}`);
getUser();
}, [location]);
In your code, the history object does not change and the effect is only fired once, the reason you keep getting the console logs when the location changes is that you added a listener to the history.

Can't perform a React state update on an unmounted component when using useEffect hook

i seem to be getting the following error when using useEffect hook.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I believe it has something to do with the async function i am calling to set if the user is authenticated or not.
ProtectedRoute.tsx
export function ProtectedRoute({ ...routeProps }: ProtectedRouteProps): ReactElement | null {
const context = useContext(AppContext);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
isUserAuthenticated(context.token).then(setIsAuthenticated).catch(setIsAuthenticated);
});
if (isAuthenticated) {
return <Route {...routeProps} />;
} else {
return <Redirect to={{ pathname: "login" }} />;
}
}
const isUserAuthenticated = async (token: any): Promise<boolean> => {
try {
const response = await VerifyAuthentication(token);
console.log("VerifyAuthentication", response);
if (response.status !== 200) {
return false;
}
return true;
} catch (err) {
return false;
}
};
App.tsx
class App extends Component<Props, State> {
renderRouter = () => {
return (
<Router>
<Switch>
<ProtectedRoute exact path="/" component={Dashboard} />
<Route exact path="/login" component={Login} />
</Switch>
</Router>
);
};
render(): ReactElement {
return (
<div className="App">
<AppProvider>
<Theme>
<Sidebar>{this.renderRouter()}</Sidebar>
</Theme>
</AppProvider>
</div>
);
}
}
Presumably this redirects the user to a route which doesn't have this component:
return <Redirect to={{ pathname: "login" }} />;
Which means the component is unmounted, or generally unloaded from active use/memory. And this always happens, because this condition will never be true:
if (isAuthenticated) {
Because when the component first renders that value is explicitly set to false:
const [isAuthenticated, setIsAuthenticated] = useState(false);
So basically what's happening is:
You fire off an asynchronous operation to check if the user is authenticated.
Before waiting for the response, you decide that the user is not authenticated and redirect them.
The component is unloaded because the user has left this page.
The asynchronous response is received and tries to update state for a component that is no longer loaded/mounted.
It's not entirely clear how this component is intended to fit into your overall structure, but you're going to need to change that structure. Either checking for authentication would need to be synchronous or you'd need to wait for the asynchronous operation to complete before redirecting. An example of the latter could be as simple as:
export function ProtectedRoute({ ...routeProps }: ProtectedRouteProps): ReactElement | null {
const context = useContext(AppContext);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
isUserAuthenticated(context.token)
.then(x => {
setIsAuthenticated(x);
setIsLoading(false);
})
.catch(e => {
setIsAuthenticated(false);
setIsLoading(false);
console.log(e);
});
});
if (isLoading) {
return <div>Loading...</div>;
} else if (isAuthenticated) {
return <Route {...routeProps} />;
} else {
return <Redirect to={{ pathname: "login" }} />;
}
}
In that scenario a separate state value of isLoading is used to track whether the asynchronous operation is still taking place, so the component "waits" until the data is loaded before deciding to redirect the user or not.
But overall I don't see why the authentication check can't be synchronous. Something higher-level, such as a provider component that wraps the entire application structure within <App/>, can have this same logic above, essentially performing the async operation and keeping the result in state. Then that state can be provided via useContext or Redux or even just passing as props to all child components.
You shouldn't need to re-check for authentication over and over in child components. That's an application-level concern.
You can use a variable to check component is mount or unmount when call setIsAuthenticated
useEffect(() => {
let isMouted = true;
isUserAuthenticated(context.token)
.then((val) => isMouted && setIsAuthenticated(val))
.catch(setIsAuthenticated);
return () => {
isMouted = false;
};
});

My Login component flashes whenever I reload my dashboard

I'm using reactjs to build a login/register system with authentication and authorization. if authenticated(jsonwebtoken), it should route me to the dashboard else redirect me back to login.
but whenever I reload it hits the login endpoint for a second then back to dashboard. how can I fix this?
Below is a giphy to show what I'm talking about
Here are the components associated with the issue stated above
App.js
const App = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false)
// set isAuthenticated to true or false
const setAuth = (boolean) => {
setIsAuthenticated(boolean)
}
useEffect(() => {
// check if the person is still Authenticated
const isAuth = async () => {
try {
const res = await fetch('/auth/verify', {
method: 'GET',
headers: { token: localStorage.token},
})
const data = await res.json()
// if authenticated, then
if(data === true) {
await setIsAuthenticated(true)
} else {
await setIsAuthenticated(false)
}
} catch (err) {
console.error(err.message)
}
}
isAuth()
})
return (
<Fragment>
<Router>
<div className='container'>
<Switch>
<Route exact path='/login' render={props => !isAuthenticated ? <Login {...props} setAuth={setAuth} /> : <Redirect to='/dashboard' /> } />
<Route exact path='/register' render={props => !isAuthenticated ? <Register {...props} setAuth={setAuth} /> : <Redirect to='/login' />} />
<Route exact path='/dashboard' render={props => isAuthenticated ? <Dashboard {...props} setAuth={setAuth} /> : <Redirect to='/login' /> } />
</Switch>
</div>
</Router>
</Fragment>
);
Login Component
const Login = ({ setAuth }) => {
const [text, setText] = useState({
email: '',
password: ''
})
const { email, password } = text
const onChange = e => setText({ ...text, [e.target.name]: e.target.value})
const onSubmit = async (e) => {
e.preventDefault()
try {
// Get the body data
const body = { email, password }
const res = await fetch('/auth/login', {
method: 'POST',
headers: {"Content-Type": "application/json"},
body: JSON.stringify(body)
})
const data = await res.json()
if(data.token) {
// save token to local storage
localStorage.setItem("token", data.token)
setAuth(true)
toast.success('Login Successful')
} else {
setAuth(false)
toast.error(data)
}
} catch (err) {
console.error(err.message)
}
}
return (
<Fragment>
<h1 className='text-center my-5'>Login</h1>
<form onSubmit={onSubmit}>
Dashboard Component
const Dashboard = ({ setAuth }) => {
const [name, setName] = useState('')
useEffect(() => {
const getName = async () => {
try {
const res = await fetch('/dashboard', {
method: 'GET',
// Get the token in localStorage into the header
headers: { token: localStorage.token }
})
const data = await res.json()
setName(data.user_name)
} catch (err) {
console.error(err.message)
}
}
getName()
// eslint-disable-next-line
}, [])
// Log out
const logOut = (e) => {
e.preventDefault()
localStorage.removeItem("token")
setAuth(false)
toast.success('Logged Out')
}
return (
<Fragment>
<h1 className='mt-5'>Dashboard</h1>
<p>Hello, {name}</p>
<button className='btn btn-primary my-3' onClick={e => logOut(e)}>Log Out</button>
</Fragment>
There are two problems that I found in your code above.
The first is that your ueEffect does not specify any dependency.
When the dependencies are not specified in this way the useEffect would run anytime any state changes.
useEffect(()=> {
// code here
}); // this one would run anytime any state changes in the component. You usually don't want this.
When a dependency array is specified, the code in the useEffect would run anytime any of the state in the dependencies changes.
useEffect(()=> {
// code here
},
[state1, state2, ...others...] //code would run when any of the state in this array changes
In your case, however, you probably want to run that useEffect once. To do this we add an empty array as the dependency value.
useEffect(()=> {
// code here
},
[] //empty deps means that the code runs only once. When the component mounts
)
Extra ideas
I also suggest that you add a loading state to your component so that you can show a loader while the API call is being made.
You might want to show a loader while the API call is being made(or even set this state to true by default since the API call is the first thing you do in your app)
.
Also, consider putting useEffect in a custom Hook

How to keep authenticated state on refresh?

I'm using firebase authentication for my app. I used useAuth hook from here. Integrate with react-router guide about redirect (Auth).
SignIn,SignOut function is working as expected. But when I try to refresh the page. It redirects to /login again.
My expected: Redirect to / route when authenticated.
I tried to add this code in PrivateRoute.js
if (auth.loading) {
return <div>authenticating...</div>;
}
So I can refresh the page without redirect to /login but it only show authenticating... when click the log out button.
Here is my code: https://codesandbox.io/s/frosty-jennings-j1m1f?file=/src/PrivateRoute.js
What I missed? Thanks!
Issue
Seems you weren't rendering the "authenticating" loading state quite enough.
I think namely you weren't clearing the loading state correctly in the useEffect in useAuth when the initial auth check was resolving.
Solution
Set loading true whenever initiating an auth check or action, and clear when the check or action completes.
useAuth
function useProvideAuth() {
const [loading, setLoading] = useState(true); // <-- initial true for initial mount render
const [user, setUser] = useState(null);
// Wrap any Firebase methods we want to use making sure ...
// ... to save the user to state.
const signin = (email, password) => {
setLoading(true); // <-- loading true when signing in
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((response) => {
setUser(response.user);
return response.user;
})
.finally(() => setLoading(false)); // <-- clear
};
const signout = () => {
setLoading(true); // <-- loading true when signing out
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
})
.finally(() => setLoading(false)); // <-- clear
};
// Subscribe to user on mount
// Because this sets state in the callback it will cause any ...
// ... component that utilizes this hook to re-render with the ...
// ... latest auth object.
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
setUser(false);
}
setLoading(false); // <-- clear
});
// Cleanup subscription on unmount
return () => unsubscribe();
}, []);
// Return the user object and auth methods
return {
loading,
user,
signin,
signout
};
}
Check the loading state in PrivateRoute as you were
function PrivateRoute({ children, ...rest }) {
const auth = useAuth();
if (auth.loading) return "authenticating";
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
Demo
Try this approach, it works for me :
const mapStateToProps = state => ({
...state
});
function ConnectedApp() {
const [auth, profile] = useAuth()
const [isLoggedIn, setIsLoggedIn] = useState(false)
useEffect(() => {
if (auth && auth.uid) {
setIsLoggedIn(true)
} else {
setIsLoggedIn(false)
}
}, [auth, profile]);
return (<Router>
<Redirect to="/app/home"/>
<div className="App">
<Switch>
<Route path="/home"><Home/></Route>
<Route path="/login"><Login styles={currentStyles}/></Route>
<Route path="/logout"><Logout styles={currentStyles}/></Route>
<Route path="/signup" render={isLoggedIn
? () => <Redirect to="/app/home"/>
: () => <Signup styles={currentStyles}/>}/>
<Route path="/profile" render={isLoggedIn
? () => <Profile styles={currentStyles}/>
: () => <Redirect to="/login"/>}/>
</Switch>
</div>
</Router>);
}
const App = connect(mapStateToProps)(ConnectedApp)
export default App;

How do you wrap a React state variable in a function call?

I have some state that gets updated async that I would like to wrap inside a function call (to modularize the code).
Concretely, I have a isLoggedIn state variable that I would like to retrieve with a function call inside a React component. So far I have:
firebase.js
function userLoggedIn() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
firebase.auth().onAuthStateChanged((user) => {
if (user == null) {
console.log("No user logged in.");
} else {
console.log(`User ${user.uid} authenticated!`);
setIsLoggedIn(true);
}
});
return isLoggedIn;
}
export default userLoggedIn;
And I have a separate component that I would like to get this isLoggedIn state from:
AuthenticatedRoute.js
const AuthenticatedRoute = ({ component: Component, ...rest }) => {
const isLoggedin = userLoggedIn();
return (
<Route
{...rest}
render={(props) =>
isLoggedIn ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
)
}
/>
);
};
It works fine on page load, but if I log out (and user = null) isLoggedIn doesn't change to False.
This all works fine if I inline the function body inside AuthenticatedRoute: in general, how does one achieve this?

Resources