I am using the useEffect() hook in my functional App component to check if the authentication has expired, so that I can dispatch an action to delete the persisted authentication state (using redux-persist). below is the code:
import React, { useEffect } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { signout } from "./app/state/authSlice";
import Signin from "./app/pages/Signin";
import Landing from "./app/pages/Landing";
const App = (props) => {
const dispatch = useDispatch();
const auth = useSelector((state) => state.auth);
const expired = new Date(Date.now()) >= new Date(auth.expires);
useEffect(() => {
const main = () => {
if (expired) {
console.log("Auth expired.");
dispatch(signout);
}
};
main();
}, [dispatch, expired]);
return (
<Router>
<Switch>
<Route exact path="/" {...props} component={Landing} />
<Route exact path="/signin" {...props} component={Signin} />
</Switch>
</Router>
);
};
export default App;
Now, I am getting the Auth expired console log when the expiry time is past, but the dispatch is not happening and my state still persists after the expiry time. I know that the signout action is correctly configured because I am using that in another component onClick.
This was just a typo. I forgot to call the signout() action creator.
Correct code below.
import React, { useEffect } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { signout } from "./app/state/authSlice";
import SigninPage from "./app/pages/Signin";
import LandingPage from "./app/pages/Landing";
const App = (props) => {
const dispatch = useDispatch();
const auth = useSelector((state) => state.auth);
const expired = new Date(Date.now()) >= new Date(auth.expires);
useEffect(() => {
const main = () => {
if (expired) {
console.log("Auth expired.");
dispatch(signout());
}
};
main();
}, [dispatch, expired]);
return (
<Router>
<Switch>
<Route exact path="/" {...props} component={LandingPage} />
<Route exact path="/signin" {...props} component={SigninPage} />
</Switch>
</Router>
);
};
export default App;
Related
I'm trying to use a context in my react project but i dont know why the component "SideBar" can't update the context. The user flow is a user and passwrod simple form in the Login component and if the credentials are valid i want to display the username in the sidebar of my dashboard, as you see this is nothing fancy.
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { UserProvider } from './contexts/user.context';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<UserProvider>
<App />
</UserProvider>
</BrowserRouter>
</React.StrictMode>
);
reportWebVitals();
App.jsx
import './App.css';
import { Routes, Route } from 'react-router-dom';
import Home from './routes/home/Home.component';
import Login from './routes/auth/Login.component';
import Dashboard from './components/Usuarios/UsuarioValid/MainColumn/Dashboard.component';
import Infrastructura from './components/Usuarios/Anonimo/MainColumn/Infrastructura.component';
import EquipoDirectivo from './components/Usuarios/Anonimo/MainColumn/EquipoDirectivo.component';
import AsignaturasRamas from './components/Usuarios/Anonimo/MainColumn/AsignaturasRamas.component';
const App = () => {
return (
<Routes>
<Route path="/" element={<Login />} />
<Route path="anonimo" element={<Home isAnonimo />} >
<Route index element={<Infrastructura />} />
<Route path="equipo" element={<EquipoDirectivo/>} />
<Route path="asignaturas" element={<AsignaturasRamas/>} />
</Route>
<Route path="dashboard" element={<Home isAnonimo={false} />} >
<Route index element={<Dashboard />} />
</Route>
</Routes>
)
}
export default App;
Home.component.jsx
import React from 'react'
import { Outlet } from 'react-router-dom';
import Sidebar from '../../components/Usuarios/UsuarioValid/Sidebar.component';
import TransitionRoot from '../../components/Usuarios/UsuarioValid/TransitionRoot.component';
import SidebarAnonimo from '../../components/Usuarios/Anonimo/SidebarAnonimo.component';
import TransitionRootAnonimo from '../../components/Usuarios/Anonimo/TransitionRootAnonimo.component';
const Home = (props) => {
const { isAnonimo } = props;
return (
<>
{
isAnonimo ? (
<div>
<TransitionRootAnonimo />
<SidebarAnonimo />
<Outlet />
</div>
)
: (
<div>
<TransitionRoot />
<Sidebar />
<Outlet />
</div>
)}
</>
)
}
export default Home;
user.context.jsx
import { createContext, useState} from "react";
// Actual value we want to access
export const UserContext = createContext({
currentUser: null,
setCurrentUser: (x) => x,
});
// Used in route file (index.js) to handle context
export const UserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const value = { currentUser, setCurrentUser };
return <UserContext.Provider value={value}> {children} </UserContext.Provider>
}
login.component.jsx
import { useState, useContext } from "react";
import { UserContext } from '../../contexts/user.context';
const Login = () => {
const { currentUser, setCurrentUser } = useContext(UserContext)
const [ formFields, setFormFields ] = useState(defaultFormFields);
const { usuario, password } = formFields;
const [ validCredentials, setValidCredentials ] = useState(true);
const handleSubmit = async (event) => {
event.preventDefault();
const usuariValid = usuarios.find(u =>
u.usuario === usuario
&& u.contraseƱa === password
);
if(usuariValid){
setCurrentUser(usuariValid);
console.log(currentUser)
/* window.location.replace('/dashboard'); */
} else {
resetFormFields();
setValidCredentials(false);
console.info("Credenciales invalidas");
}
};
/*NOT RELEVANT CODE*/
sidebar.component.jsx
*
import { UserContext } from '../../../contexts/user.context'
import React, {useState, useEffect, useContext, Fragment} from 'react'
const Sidebar = () => {
const { currentUser } = useContext(UserContext);
const [idAsignaturas, setIdAsignaturas] = useState([]);
const [nombreAsignaturas, setNombreAsignaturas] = useState([])
useEffect(()=>{ console.log(currentUser},[currentUser]) /*OUTPUT:null*/
/*NOT RELEVANT CODE*/
}*
The code is correct, the problem is that the Context in react is useful for components that are mounted at the same time. In our case we were trying to use the context in components that are not mounted at the same time, because the component "Sidebar" is loaded after the user logs in "Login". So when the user logs in, the pages are reloaded and "Sidebar" is mounted, when this happens the context restarts and loses the values that had been updated in the "Login" component.
I'm new to ReactJS and am building a basic application. Here I'm using protected router and implementing an authorization mechanism with useContext and local storage. The aim is to redirect users that are not logged in to the Login Page if they attempt to access Dashboard.
After I do log in, the access token is saved to local storage and account info is saved in auth context. Then I go to Dashboard and I reload the page. I implemented a useEffect hook to check for token in local storage and I thought that when I reload at the Dashboard page, the auth provider would check for the token and return a result that I'm authenticated. However it doesn't work as expected so I am redirected to Login page (Although the useEffect callback was triggered)
Below is my code:
src\components\App\index.js
import { Routes, Route } from 'react-router-dom';
import Login from '../Login';
import Signup from '../Signup';
import GlobalStyles from '../GlobalStyles';
import ThemeProvider from 'react-bootstrap/ThemeProvider';
import RequireAuth from '../RequireAuth';
import Layout from '../Layout';
import Dashboard from '../Dashboard';
import Account from '../Account';
function App() {
return (
<GlobalStyles>
<ThemeProvider>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route element={<RequireAuth />}>
<Route path="/" element={<Layout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/account" element={<Account />} />
</Route>
</Route>
</Routes>
</ThemeProvider>
</GlobalStyles>
);
}
export default App;
src\components\RequireAuth\index.js
import { useLocation, Navigate, Outlet } from 'react-router-dom';
import useAuth from '../../hooks/useAuth';
function RequireAuth() {
const { auth } = useAuth();
const location = useLocation();
return auth?.user ? (
<Outlet />
) : (
<Navigate to={{ pathname: '/', state: { from: location } }} replace />
);
}
export default RequireAuth;
src\hooks\useAuth.js
import { useContext } from 'react';
import { AuthContext } from '../context/AuthProvider';
function useAuth() {
return useContext(AuthContext);
}
export default useAuth;
src\context\AuthProvider.js
import { useEffect } from 'react';
import { createContext, useState } from 'react';
import api from '../helper/api';
const AuthContext = createContext({});
function AuthProvider({ children }) {
const [auth, setAuth] = useState({});
useEffect(() => {
const apiHelper = new api();
apiHelper.getAccountInfo().then((response) => {
setAuth(response.data);
});
}, []);
console.log(auth.user);
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
);
}
export { AuthContext, AuthProvider };
src\components\Login\index.js
import { useState, useRef, useEffect } from 'react';
import clsx from 'clsx';
import { Form, FormGroup, Button } from 'react-bootstrap';
import Alert from 'react-bootstrap/Alert';
import FloatingLabel from 'react-bootstrap/FloatingLabel';
import { useNavigate } from 'react-router';
import styles from './style.module.scss';
import logo from '../../assets/images/logo.png';
import LoadingSpinner from '../LoadingSpinner';
import api from '../../helper/api';
import useAuth from '../../hooks/useAuth';
const Login = () => {
const usernameRef = useRef();
const errorRef = useRef();
const [state, setState] = useState({
username: '',
password: ''
});
const { username, password } = state;
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
usernameRef.current.focus();
}, []);
const { setAuth } = useAuth();
const navigate = useNavigate();
const submitForm = (event) => {
event.preventDefault();
setLoading(true);
const apiHelper = new api();
apiHelper
.login({
username,
password
})
.then((response) => {
setLoading(false);
setError();
setAuth({ user: response.data.user });
localStorage.setItem('token', response.data.token);
navigate('/dashboard');
})
.catch((error) => {
setLoading(false);
setError(error.response.data.message);
usernameRef.current.focus();
});
};
return (
/** Some hmtml */
);
};
export default Login;
A video on how the error occurs: https://streamable.com/b1cp1t
Can anyone tell me where I'm wrong and how to fix it? Many thanks in advance!
you can think about a <Route> kind of like an if statement, if its path matches the current URL, it renders its element!
since the path of your first route in the list is "/" to the login page matches the need of the router it will redirect you there.
so, if you will delete this line:
<Route path="/" element={<Login />} />
and let the <RequireAuth /> take care of the path="/" it will check first if the user is logged in, if so let him continue.
if not it will redirect to "/login"
I am trying to create my own custom authentication using React, AWS Amplify, and React Router V6, and my goal is to protect certain routes so users that are not logged in can't access them.
My code is here:
import './App.css';
import { ThemeProvider, createTheme } from '#mui/material/styles';
import Navbar from './components/Navbar/Navbar';
import Dashboard from './components/Dashboard/Dashboard';
import Reports from './components/Reports/Reports';
import Patients from './components/Patients/Patients';
import { BrowserRouter, Navigate, Outlet, Route, Routes, useLocation, } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import '#aws-amplify/ui-react/styles.css';
import Signup from './components/Signup/Signup';
import Signin from './components/Signin/Signin';
import { Auth } from 'aws-amplify';
const darkTheme = createTheme({
palette: {
mode: 'dark',
}
});
const useAuth = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
})
return { user };
}
function RequireAuth() {
const auth = useAuth();
const location = useLocation();
console.log(auth);
return (
auth
? <Outlet/>
: <Navigate to='/signin' state={{ from: location}} replace/>
);
}
const App = () => {
return (
<ThemeProvider theme={darkTheme}>
<BrowserRouter>
<Routes>
<Route element={<RequireAuth />}>
<Route path="/" element={<Navbar />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="patients" element={<Patients />} />
<Route path="reports" element={<Reports />} />
</Route>
</Route>
<Route path="/signin" element={<Signin />} />
<Route path="/signup" element={<Signup />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
);
}
export default App;
I have spent the whole day trying countless methods but I keep getting the same results. What I'm trying to do is protect 3 routes Dashboard, Patients, and Reports. Whenever I click the sign-in button, I get around 500 logs of the current user as shown below:
Does anyone know what is triggering this component to re-render infinitely (hence executing my console.log(user) function), and is there any solution to fix this?
It appears the useEffect hook is missing a dependency array, so its callback is triggered each render cycle. Since the effect updates state, it triggers a rerender.
Add a dependency array.
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
}, []);
If you need to run this effect more than once when the routes are mounted, then you may need to add a dependency, like location if/when the route path changes.
Example:
const { pathname } = useLocation();
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
}, [pathname]);
Since the useEffect hook runs at the end of the initial render cycle you may want to also conditionally wait to render the outlet or redirect until the user state is populated.
Example:
const useAuth = () => {
const [user, setUser] = useState(); // <-- initially undefined
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
}, []);
return { user };
}
...
function RequireAuth() {
const auth = useAuth();
const location = useLocation();
if (auth === undefined) {
return null; // or loading indicator, etc...
}
return (
auth
? <Outlet/>
: <Navigate to='/signin' state={{ from: location }} replace/>
);
}
I want to dispatch an action whenever any page or component of my app loads. So what I am trying to do is to dispatch the action using store.dispatch inside useEffect hook in main App.js file.
When I try, it gives a Maximum update depth exceeded Error
I have seen it done in a tutorial or two, I just cant find what I am doing wrong.
Is there any other way of achieving this?
Please take a look!
Here is my App.js
import React, { useEffect } from "react";
import { Provider } from "react-redux";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Login from "./pages/Login";
import Landing from "./pages/Landing";
import Contact from "./pages/Contact";
import Dashboard from "./pages/Dashboard";
import store from "./store";
import { loadUser } from "./tasks/authT";
import setAuthToken from "./utils/setAuthToken";
import PrivateRoutes from "./utils/PrivateRoutes";
if (localStorage.token) {
setAuthToken(localStorage.token);
}
const App = () => {
useEffect(() => {
store.dispatch(loadUser());
}, []);
return (
// <Landing />
<Provider store={store}>
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Landing />} />
<Route exact path="/contact" element={<Contact />} />
<Route exact path="/login" element={<Login />} />
<Route
exact
path="/dashboard"
element={
<PrivateRoutes>
<Dashboard />
</PrivateRoutes>
}
/>
</Routes>
</BrowserRouter>
</Provider>
);
};
export default App;
this is the action I am trying to dispatch
export const loadUser = () => async (dispatch) => {
if (localStorage.token) {
setAuthToken(localStorage.token);
}
try {
const res = await axios.get("/api/auth");
dispatch({
type: USER_LOADED,
payload: res.data,
});
} catch (error) {
dispatch({
type: AUTH_ERROR,
});
}
};
Try to use hook useDispatch from react-redux. Also wrap your App.js in <Provider store={store}> .
index.js
<Provider store={store}>
<App />
</Provider>
App.js
import { useDispatch } from 'react-redux';
const App = () => {
const dispatch = useDispatch();
...
useEffect(() => {
loadUser()(dispatch);
}, []);
...
}
That error occurs because:
if (localStorage.token) {
setAuthToken(localStorage.token);
}
that cause a component render, so useEffect triggers and dispatch loadUser().
useEffect(() => {
store.dispatch(loadUser());
}, []);
it triggers because the dependency array is empty.
I suggest to add authToken as a dependency in the array.
useEffect(() => {
store.dispatch(loadUser());
}, [authToken]);
I am migrating an app from Firebase to AWS Amplify. I want to create a React context which will provide route protection if the user is not logged in.
For example, my Auth.js file:
import React, { useEffect, useState, createContext } from 'react'
import fire from './firebase'
export const AuthContext = createContext()
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null)
useEffect(() => {
fire.auth().onAuthStateChanged(setCurrentUser)
}, [])
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
)
}
And my App.js file:
import * as React from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Navbar from './components/navbar/navbar'
import Home from './routes/Home'
import Register from './routes/Register'
import Footer from './components/footer/Footer'
import AlertProvider from './components/notification/NotificationProvider'
import MyAlert from './components/notification/Notification'
import { AuthProvider } from './Auth'
import PrivateRoute from './PrivateRoute'
const App = () => {
return (
<AuthProvider>
<BrowserRouter>
<AlertProvider>
<div className="app">
<Navbar />
<MyAlert />
<Switch>
<Route path="/" exact component={Home} />
<Route
path="/register"
exact
component={Register}
/>
<Route
path="/forgot-password"
render={(props) => <div>Forgot Password</div>}
/>
<Route path="*" exact={true} component={Home} />
</Switch>
<Footer />
</div>
</AlertProvider>
</BrowserRouter>
</AuthProvider>
)
}
export default App
This all works fine.
How would I do something similar with AWS Amplify? Essentially how would I create a Auth.js file that would wrap around my routes and give them a user context (which would update when the authentication status for the user is changed).
Thanks!
You can achieve this by setting up a custom protectedRoute HOC that will be used to protect any route that requires authentication. It will check if the user is signed-in and if the user is not signed-in then it will re-direct them to a specified route.
protectedRoute.js
import React, { useEffect } from 'react'
import { Auth } from 'aws-amplify'
const protectedRoute = (Comp, route = '/profile') => (props) => {
async function checkAuthState() {
try {
await Auth.currentAuthenticatedUser()
} catch (err) {
props.history.push(route)
}
}
useEffect(() => {
checkAuthState()
})
return <Comp {...props} />
}
export default protectedRoute
You can specify the default route or another route like the following:
// default redirect route
export default protectedRoute(Profile)
// custom redirect route
export default protectedRoute(Profile, '/sign-in')
You could also use the pre-built HOC from aws-amplify called withAuthenticator and that provides the UI as well as checking the users authentication status.
Sample use case for a profile page:
import React, { useState, useEffect } from 'react'
import { Button } from 'antd'
import { Auth } from 'aws-amplify'
import { withAuthenticator } from 'aws-amplify-react'
import Container from './Container'
function Profile() {
useEffect(() => {
checkUser()
}, [])
const [user, setUser] = useState({})
async function checkUser() {
try {
const data = await Auth.currentUserPoolUser()
const userInfo = { username: data.username, ...data.attributes, }
setUser(userInfo)
} catch (err) { console.log('error: ', err) }
}
function signOut() {
Auth.signOut()
.catch(err => console.log('error signing out: ', err))
}
return (
<Container>
<h1>Profile</h1>
<h2>Username: {user.username}</h2>
<h3>Email: {user.email}</h3>
<h4>Phone: {user.phone_number}</h4>
<Button onClick={signOut}>Sign Out</Button>
</Container>
);
}
export default withAuthenticator(Profile)
The routing for both would be the same and below I have linked a sample that I have used for both.:
import React, { useState, useEffect } from 'react'
import { HashRouter, Switch, Route } from 'react-router-dom'
import Nav from './Nav'
import Public from './Public'
import Profile from './Profile'
import Protected from './Protected'
const Router = () => {
const [current, setCurrent] = useState('home')
useEffect(() => {
setRoute()
window.addEventListener('hashchange', setRoute)
return () => window.removeEventListener('hashchange', setRoute)
}, [])
function setRoute() {
const location = window.location.href.split('/')
const pathname = location[location.length-1]
setCurrent(pathname ? pathname : 'home')
}
return (
<HashRouter>
<Nav current={current} />
<Switch>
<Route exact path="/" component={Public}/>
<Route exact path="/protected" component={Protected} />
<Route exact path="/profile" component={Profile}/>
<Route component={Public}/>
</Switch>
</HashRouter>
)
}
export default Router