PrivateRoute functional component using aws-amplify as authentication - javascript

I'm trying to create a functional component equivalent of the following PrivateRoute class component (source code here):
import React, { useState, useEffect } from "react";
import { Route, Redirect, withRouter, useHistory } from "react-router-dom";
import { Auth } from "aws-amplify";
class PrivateRoute extends React.Component {
state = {
loaded: false,
isAuthenticated: false
};
componentDidMount() {
this.authenticate();
this.unlisten = this.props.history.listen(() => {
Auth.currentAuthenticatedUser()
.then(user => console.log("user: ", user))
.catch(() => {
if (this.state.isAuthenticated)
this.setState({ isAuthenticated: false });
});
});
}
componentWillUnmount() {
this.unlisten();
}
authenticate() {
Auth.currentAuthenticatedUser()
.then(() => {
this.setState({ loaded: true, isAuthenticated: true });
})
.catch(() => this.props.history.push("/auth"));
}
render() {
const { component: Component, ...rest } = this.props;
const { loaded, isAuthenticated } = this.state;
if (!loaded) return null;
return (
<Route
{...rest}
render={props => {
return isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/auth"
}}
/>
);
}}
/>
);
}
}
export default withRouter(PrivateRoute);
The above code works in my application when I use it like so:
<PrivateRoute
exact
path={urls.homepage}
component={Homepage}
/>
My attempt at a converting the above class component into a functional component is the following:
import React, { useState, useEffect } from "react";
import { Route, Redirect, useHistory } from "react-router-dom";
import { Auth } from "aws-amplify";
const PrivateRoute = ({ component: Component, ...rest }) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
let history = useHistory();
useEffect(() => {
Auth.currentAuthenticatedUser()
.then(() => {
setIsLoaded(true);
setIsAuthenticated(true);
})
.catch(() => history.push("/auth"));
return () =>
history.listen(() => {
Auth.currentAuthenticatedUser()
.then(user => console.log("user: ", user))
.catch(() => {
if (isAuthenticated) setIsAuthenticated(false);
});
});
}, [history, isAuthenticated]);
if (!isLoaded) return null;
return (
<Route
{...rest}
render={props => {
return isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/auth"
}}
/>
);
}}
/>
);
};
export default PrivateRoute;
But when using my functional component in the same way, I keep getting the following error:
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
It always redirects me to /auth, regardless of whether I am logged in or not. What am I doing wrong? Any help is much appreciated!

I think you are missing your unmount, the return in the useEffect should be your unlisten which is the unmount. Also, I removed useHistory and pulled history from the props and used withRouter
Try this
import React, { useState, useEffect } from "react";
import { Route, Redirect, withRouter } from "react-router-dom";
import { Auth } from "aws-amplify";
const PrivateRoute = ({ component: Component, history, ...rest }) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
Auth.currentAuthenticatedUser()
.then(() => {
setIsLoaded(true);
setIsAuthenticated(true);
})
.catch(() => history.push("/auth"));
const unlisten = history.listen(() => {
Auth.currentAuthenticatedUser()
.then(user => console.log("user: ", user))
.catch(() => {
if (isAuthenticated) setIsAuthenticated(false);
});
});
return unlisten();
}, [history, isAuthenticated]);
if (!isLoaded) return null;
return (
<Route
{...rest}
render={props => {
return isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/auth"
}}
/>
);
}}
/>
);
};
export default withRouter(PrivateRoute);

Try this out:
useEffect(() => {
async function CheckAuth() {
await Auth.currentAuthenticatedUser()
.then((user) => {
setIsLoaded(true);
setIsAuthenticated(true);
})
.catch(() => history.push("/auth"));
}
CheckAuth();
}, []);

Related

React-Router-Dom unable to render page but routes back due to PrivateRoute

I am having some issues with my routing currently when authenticated. Whenever I try to access my ViewPortfolio page at localhost:3000/portfolio/portfolioId it will redirect me back to my homepage. I am not sure what is going on. I have also tried manipulating the URL by modifying it to the correct URL link but it also redirects me back to /homepage when I am authenticated. The source codes can be found below. App.js is my router with PrivateRoute as the private route component and finally, CreateInterview.js where I redirect using js windows.location function to ViewPortfolio.js which will use useParams() react hook to get the param. But instead now after creating successfully and redirect to the correct URL with the portfolioId it will redirect back to homepage within less than a second.
PrivateRoute.js
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
const { currentUser } = useAuth()
return (
<Route
{...rest}
render={(props) => {
if (currentUser) {
return <Component {...props} />
} else {
return <Redirect to={{
pathname: "/",
state:{
from: props.location
}
}}/>
}
}
}>
</Route>
)
}
export default PrivateRoute
App.js
import React from "react"
.
.
.
import PublicRoute from "./PublicRoute";
function App() {
return (
<AuthProvider>
<Router>
<Switch>
{/* Auth Routes */}
<PublicRoute exact path='/' component={Login} />
.
.
.
<PrivateRoute exact path='/createInterview' component={CreateInterview} />
<PrivateRoute path='/manageInterview' component={ManageInterview} />
<PrivateRoute path='/portfolio/:portfolioId' component={ViewPortfolio} />
{/* Non-Existance Routes */}
<Route path="*" component={() => "404 NOT FOUND"} />
</Switch>
</Router>
</AuthProvider>
)
}
export default App
CreatInterview.js redirecting in js (onSubmit of the form)
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
await database.portfolioRef.add({
intervieweeName: intervieweeNameRef.current.value,
intervieweeEmail: intervieweeEmailRef.current.value,
intervieweeMobileNumber: intervieweeMobileRef.current.value,
projectTitle: projectTitleRef.current.value,
portfolioTitle: portfolioNameRef.current.value,
dateCreated: new Date().toLocaleString('en-SG'),
createdBy: currentUser.displayName
}).then(function(docRef) {
console.log("This is the Document ID " + docRef.id.toString());
console.log(docRef.id);
window.location = '/portfolio/' + docRef.id;
})
setLoading(false)
}
Part of ViewPortfolio.js to receive the portfolioId from CreateInterview.js
const ViewPortfolio = () => {
let { portfolioId } = useParams();
AuthContext.js
import React, { useContext, useState, useEffect } from "react"
import { auth, database } from "../firebase";
import { getDocs, query, where } from "firebase/firestore";
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(null)
const [loading, setLoading] = useState(true)
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password).then(() => {
const Doc = query(database.usersRef, where("email", "==", email));
getDocs(Doc).then((querySnapshot) => {
let values = '';
querySnapshot.forEach((doc) => {
values = doc.id;
});
var userUpdate = database.usersRef.doc(values);
userUpdate.update({
lastActive: new Date().toLocaleString('en-SG'),
})
})
});
}
function logout() {
return auth.signOut();
}
function forgetPassword(email) {
return auth.sendPasswordResetEmail(email);
}
function updateEmail(email) {
return currentUser.updateEmail(email)
}
function updatePassword(password) {
return currentUser.updatePassword(password)
}
function updateDisplayName(name) {
return currentUser.updateDisplayName(name)
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged( user => {
setLoading(false)
setCurrentUser(user)
})
return unsubscribe
}, [])
const value = {
currentUser,
login,
forgetPassword,
logout,
updateEmail,
updatePassword,
updateDisplayName,
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
The initial currentUser state matches the unauthenticated state, so when the app initially renders, if you are accessing a protected route the redirection will occur because the currentUser state hasn't updated yet.
Since onAuthStateChanged returns null for unauthenticated users then I suggest using anything other than null for the initial currentUser state. undefined is a good indeterminant value. You can use this indeterminant value to conditionally render a loading indicator, or nothing at all, while the auth status is confirmed on the initial render.
AuthProvider
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(); // <-- undefined
...
PrivateRoute
const PrivateRoute = (props) => {
const { currentUser } = useAuth();
if (currentUser === undefined) {
return null; // or loading spinner, etc...
}
return currentUser
? (
<Route {...props} />
)
: (
<Redirect
to={{
pathname: "/",
state: {
from: props.location
}
}}
/>
);
}
You should also really replace the window.location = '/portfolio/' + docRef.id; logic with a history.push('/portfolio/' + docRef.id); so you are not unnecessarily reloading the page.
const history = useHistory();
...
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
try {
const docRef = await database.portfolioRef.add({
intervieweeName: intervieweeNameRef.current.value,
intervieweeEmail: intervieweeEmailRef.current.value,
intervieweeMobileNumber: intervieweeMobileRef.current.value,
projectTitle: projectTitleRef.current.value,
portfolioTitle: portfolioNameRef.current.value,
dateCreated: new Date().toLocaleString('en-SG'),
createdBy: currentUser.displayName
});
history.push('/portfolio/' + docRef.id);
} catch (error) {
// handle error, clear loading state
setLoading(false);
}
}

How to render react routes after auth response?

When someone arrives to my webpage I first check if the user is authenticated or not. I need to wait for the response from my GET /auth/loggedin request before rendering the routes so that I know if I should redirect to /login or /. I have added conditional rendering to the App.js component, but it always redirects the user to /login even when the user is authenticated.
App.js:
import { useEffect, useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import AuthContext from "./contexts/authContext";
import { apiLoggedIn } from "./api/auth";
import ProtectedRoute from "./components/ProtectedRoute";
import Document from "./pages/Document";
import { Login } from "./pages/Login";
import { Signup } from "./pages/Signup";
import Layout from "./components/Layout";
import PublicRoute from "./components/PublicRoute";
export const App = () => {
const [auth, setAuth] = useState({ isAuth: undefined, user: undefined });
useEffect(() => {
apiLoggedIn()
.then((res) => {
setAuth({ isAuth: true, user: res.data.userData });
})
.catch((err) => {
if (err.response) {
setAuth({ isAuth: false, user: {} });
} else if (err.request) {
setAuth({ isAuth: false, user: {} });
console.log(err.request);
} else {
setAuth({ isAuth: false, user: {} });
console.log(err.message);
}
});
}, []);
return (
<>
{auth.isAuth !== undefined ? (
<AuthContext.Provider value={{ auth, setAuth }}>
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<ProtectedRoute redirectTo="/login">
<Layout />
</ProtectedRoute>
}
>
<Route index element={<Document />} />
</Route>
<Route
path="/signup"
element={
<PublicRoute>
<Signup />
</PublicRoute>
}
/>
<Route
path="/login"
element={
<PublicRoute>
<Login />
</PublicRoute>
}
/>
</Routes>
</BrowserRouter>
</AuthContext.Provider>
) : (
""
)}
</>
);
};
./api/auth.js
import axios from "axios";
export const apiLoggedIn = () => {
return axios.get("/auth/loggedin");
};
./components/ProtectedRoute.js
import { useContext } from "react";
import AuthContext from "../contexts/authContext";
import { Navigate } from "react-router-dom";
const ProtectedRoute = ({ children, redirectTo }) => {
const { isAuth } = useContext(AuthContext);
return isAuth ? children : <Navigate to={redirectTo} />;
};
export default ProtectedRoute;
./components/PublicRoute.js
import { useContext } from "react";
import AuthContext from "../contexts/authContext";
import { Navigate } from "react-router-dom";
const PublicRoute = ({ children }) => {
const { isAuth } = useContext(AuthContext);
return isAuth ? <Navigate to={"/"} /> : children;
};
export default PublicRoute;
./contexts/authContext.js
import { createContext } from "react";
const AuthContext = createContext({
auth: { isAuth: false, user: {} },
setAuth: () => {},
});
export default AuthContext;
The AuthContext value is an object with auth and setAuth properties
<AuthContext.Provider value={{ auth, setAuth }}>
...
</AuthContext.Provider>
But in the route wrapper you are referencing an isAuth property, which will always be undefined, i.e. falsey.
const ProtectedRoute = ({ children, redirectTo }) => {
const { isAuth } = useContext(AuthContext);
return isAuth ? children : <Navigate to={redirectTo} />;
};
This is why the redirect always occurs. To resolve, ensure you reference the same context values throughout the code.
Either specify an isAuth context value:
<AuthContext.Provider value={{ isAuth: auth.isAuth, setAuth }}>
...
</AuthContext.Provider>
Or fix the wrappers:
const ProtectedRoute = ({ children, redirectTo }) => {
const { auth } = useContext(AuthContext);
return auth.isAuth ? children : <Navigate to={redirectTo} />;
};
...
const PublicRoute = ({ children }) => {
const { auth } = useContext(AuthContext);
return auth.isAuth ? <Navigate to={"/"} /> : children;
};
Your context has the following shape :
{
"auth": {
"isAuth": true,
"user": {...},
},
"setAuth": function ...
}
you must update ProtectedRoute to
const ProtectedRoute = ({ children, redirectTo }) => {
const { auth } = useContext(AuthContext);
return auth.isAuth ? children : <Navigate to={redirectTo} />;
};
you must update PublicRoute to
const PublicRoute = ({ children }) => {
const { auth } = useContext(AuthContext);
return auth.isAuth ? <Navigate to={"/"} /> : children;
};
the key name is not passed correctly in auth provider it should be passed as following here is sample sandbox demo : https://codesandbox.io/s/routes-4c5oh?file=/src/App.js
<AuthContext.Provider value={{ isAuth:auth, setAuth }}>
or change the key to auth in each place. in Public and Protected routes like following
const { auth:{isAuth} } = useContext(AuthContext);

Redux resets after dispatching an action

so I've been working on this small cinema app and I'm facing a weird bug where every time I try to log in to access the home page it doesn't redirect and then the redux store gets reset, but when I commented useEffect in the HomePage.js component It worked and also when I tried to conosole.log(action.payload) in another reducer file for the movies I got the payload from the AuthReducer so I don't know what's the problem, is it with my whole redux setup or because I'm using the render method in my ProtecdedRoute.js
AuthSaga.js
function* loginUserSaga({ payload }) {
yield delay(1500);
yield put(LoginUserSuccessAction(payload));
}
export default function* AuthSaga() {
yield takeLatest(actionTypes.LOGIN_USER, loginUserSaga);
}
protectedRoute.js
import React from 'react';
import { useSelector } from 'react-redux';
import { Route, Redirect, useLocation } from 'react-router';
const ProtectedRoute = ({ component: Component, layout: Layout, ...rest }) => {
const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
if (!isLoggedIn) {
return (
<Redirect
to={{
pathname: '/login',
}}
/>
);
}
const renderComponent = () => {
return (
<Layout>
<Component />
</Layout>
);
};
return <Route {...rest} render={renderComponent} />;
};
export default ProtectedRoute;
Routes.js
const Routes = () => {
const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
console.log(isLoggedIn);
return (
<Switch>
<ProtectedRoute path="/" exact component={Home} layout={Layout} />
<AuthRoute path="/login" exact component={Login} layout={AuthLayout} />
</Switch>
);
};
export default Routes;
AuthReducer
const authReducer = (state = initialState, action) => {
const { type, payload } = action;
console.log(payload)
switch (type) {
case actionTypes.LOGIN_USER: {
return {
...state,
loading: true,
};
}
case actionTypes.LOGIN_USER_SUCCESS: {
return {
...state,
userName: payload.userName,
token: `${payload.password}${payload.userName}`,
isLoggedIn: true,
loading: false,
};
}
case actionTypes.LOGIN_USER_FAIL: {
return {
...state,
loading: false,
};
}
case actionTypes.LOGOUT_USER: {
return {
...state,
userName: null,
token: null,
isLoggedIn: false,
loading: false,
};
}
default:
return initialState;
}
};
HomePage.jsx
const Home = () => {
const dispatch = useDispatch();
const upcomingMoviesSelector = useSelector(state => state.upcomingMovies.items)
useEffect(() => {
dispatch(fetchUpcomingMoviesAction());
console.log('')
}, []);
return (
<div>
<MovieSlider items={upcomingMoviesSelector} />
</div>
);
};
export default Home;

How to fix, can't perform a React state update on an unmounted component?

I have a problem with my code. Here is my code in App.tsx
function App() {
const dispatch = useDispatch();
const auth = useSelector((state: RootStateOrAny) => state.auth);
const token = useSelector((state: RootStateOrAny) => state.token);
useEffect(() => {
const firstlogin = localStorage.getItem('firstlogin');
if(firstlogin) {
const getUserToken = async () => {
const res: any = await axios.post('/user/refresh_token', null);
dispatch({type: "GET_TOKEN", payload: res.data.access_token});
};
getUserToken();
}
}, [auth.isLogged, dispatch]);
useEffect(() => {
if(token){
const getUser = () => {
dispatch(dispatchLogin());
return fetchUser(token).then(res => {
dispatch(dispatchGetUser(res))
})
}
getUser();
}
},[token, dispatch]);
return (
<Router>
<div className="App">
<Header/>
<Body/>
</div>
</Router>
);
}
export default App;
And when I try to access the state in my Home component, which is under the Body component. It gives me an error saying: "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."
This is my Home component:
import React from 'react';
import { useSelector, RootStateOrAny } from 'react-redux';
export default function Home() {
const auth = useSelector((state: RootStateOrAny) => state.auth);
const {username} = auth.user[0];
return(
<div>
Welcome {username} !
</div>
)
};
And the Body Component:
export default function Body() {
const auth = useSelector((state: RootStateOrAny) => state.auth);
const {isLogged} = auth;
return (
<Switch>
<Route path="/signup">
{isLogged ? <Notfound/> : <Signup/>}
</Route>
<Route path="/login">
{isLogged ? <Notfound/> : <Login/>}
</Route>
<Route path="/home">
{isLogged ? <Home/> : <Login/>}
</Route>
<Redirect to="/home"/>
</Switch>
)
}
These are my reducers:
const initialState = {
user: [],
isAdmin: false,
isLogged: false,
};
const authReducer = (state = initialState, action: any) => {
switch(action.type) {
case ACTIONS.LOGIN :
return {
...state,
isLogged: true
}
case ACTIONS.GET_USER :
return {
...state,
user: action.payload.user
}
default:
return state
}
}
How can I fix this? Thank you.
if you are using conditional rendering in your routes,It leads to this kind of error.Since the component mount is defined by the conditional statement.For that you can use history.push("/") with conditional statements in the useeffect hook of your Home component.
or you can check this Can't perform a React state update on an unmounted component

Secure pages in sign in/up AWS Cognito React

I'm doing the sign in/up pages for an app, and I'm using AWS Cognito and React for the first time. I need to secure my pages until someone signs in/up. I couldn't figure out how to send anything from the userAuth() to the export default, or how to make this work.
import React, { Component } from 'react';
import {
BrowserRouter as Router,
Route,
Link,
Redirect,
withRouter
} from 'react-router-dom';
import App from '../App';
import { Auth } from 'aws-amplify';
//last thing 333
async function userAuth() {
let something = Boolean;
Auth.currentSession()
.then(function(fulfilled) {
console.log('worked' + fulfilled);
something === true;
return something;
})
.catch(function(error) {
console.log('didnt work' + error);
window.location.href = '/';
return error;
});
}
export default ({ component: C, ...rest }) => (
alert('this is the user auth ' + userAuth()),
(
<Route
{...rest}
render={
props =>
userAuth() === 'something' ? (
<Redirect to="/" />
) : (
<C {...props} />
)
}
/>
)
);
Auth.currentAuthenticatedUser() is an asynchronous API, so you cannot return anything from this API that can be used in a Route (there is the possibility of returning a Promise, but that is not actually necessary.)
You can change the component to a class like:
class PrivateRoute extends React.Component {
constructor(props) {
super(props);
this.state = {
authStatus: false,
loading: false,
}
}
componentDidMount() {
Auth.currentAuthenticatedUser()
.then((user)=> {
this.setState({ loading: false, authStatus: true });
})
.catch(() => {
this.setState({ loading: false });
this.props.history.push('/login');
});
}
render() {
return <Route {...rest} render={(props) => (
this.state.authStatus
? <Component {...props} />
: <div>Loading ... </div>
)} />
}
}
Just fixed the problem, and thought it may help someone having the same issue !
import React from 'react';
import {
withRouter,
Switch,
Route,
Redirect,
BrowserRouter as Router
} from 'react-router-dom';
import { Auth } from 'aws-amplify';
class AppliedRoute extends React.Component {
state = {
loaded: false,
isAuthenticated: false
};
componentDidMount() {
this.authenticate();
this.unlisten = this.props.history.listen(() => {
Auth.currentAuthenticatedUser()
.then(user => console.log('user: ', user))
.catch(() => {
if (this.state.isAuthenticated)
this.setState({ isAuthenticated: false });
});
});
}
componentWillUnmount() {
this.unlisten();
}
authenticate() {
Auth.currentAuthenticatedUser()
.then(() => {
this.setState({ loaded: true, isAuthenticated: true });
})
.catch(() => this.props.history.push('/'));
}
render() {
const { component: Component, ...rest } = this.props;
const { loaded, isAuthenticated } = this.state;
if (!loaded) return null;
return (
<Route
{...rest}
render={props => {
return isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/'
}}
/>
);
}}
/>
);
}
}
AppliedRoute = withRouter(AppliedRoute);
export default AppliedRoute;

Categories

Resources