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]);
Related
I'm creating a protected route for my react project but context values and redux reducers data are not persistent. So what is the optimal way to set, for example isVerified to true if the user is logged. If isVerified === true go to homepage else redirect to login, isVerified needs to be mutated every change in route or refresh, because context or redux data is not persistent.
Do I need to create a separate backend api for just checking the token coming from the client side? then I will add a useEffect in the main App.tsx, something like:
useEffect(() => {
/*make api call, and pass the token stored in the localStorage. If
verified success then: */
setIsVerified(true)
}, [])
Then I can use the isVerified to my protected route
You can create a Middleware component wrapping both, Protected and Non-protected components routes. And inside of each just check if the user is logged, then render conditionally.
This is usually how I implemented,
Protected:
// AuthRoute.js
import React, { useEffect, useState } from "react";
import { Redirect, Route } from "react-router-dom";
export const AuthRoute = ({ exact = false, path, component }) => {
const [isVerified, setIsVerified] = useState(false);
const checkLoginSession = () => {
// Write your verifying logic here
const loggedUser = window.localStorage.getItem("accessToken") || "";
return loggedUser !== "" ? true : false;
};
useEffect(() => {
(async function () {
const sessionState = await checkLoginSession();
return setIsVerified(sessionState);
})();
}, []);
return isVerified ? (
<Route exact={exact} path={path} component={component} />
) : (
<Redirect to={"/login"} />
);
};
Non-protected:
import React from "react";
import { Redirect, Route } from "react-router-dom";
import { useEffect, useState } from "react";
export const NAuthRoute = ({ exact = false, path, component }) => {
const [isVerified, setIsVerified] = useState(false);
const checkLoginSession = () => {
// Write your verifying logic here
const loggedUser = window.localStorage.getItem("accessToken") || "";
return loggedUser !== "" ? true : false;
};
useEffect(() => {
(async function () {
const sessionState = await checkLoginSession();
return setIsVerified(sessionState);
})();
}, []);
return isVerified ?
<Redirect to={"/"} />
: <Route exact={exact} path={path} component={component} />;
};
I usually use this process and so far it is the most helpful way to make a route protected.
ProtectedRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
export const ProtectedRoute = ({component: Component, ...rest}) => {
const isSessionAuth = sessionStorage.getItem('token');
return (
<Route
{...rest}
render={props => {
if(isSessionAuth)
{
return (<Component {...props}/>);
}else{
return <Redirect to={
{
pathname: "/",
state: {
from: props.location
}
}
}/>
}
}
}/>
);
};
In the place where you are defining routes you can use this like this.
import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { ProtectedRoute } from './ProtectedRoute.js';
import SignUp from "./pages/SignUp";
import SignIn from "./pages/SignIn";
import Dashboard from "./pages/Dashboard";
import NotFound from "./pages/NotFound";
const Routes = () => (
<BrowserRouter>
<Switch>
<Route exact path="/" component={SignIn} />
<ProtectedRoute exact path="/dashboard" component={Dashboard} />
<Route path="/signup" component={SignUp} />
<Route path="*" component={NotFound} />
</Switch>
</BrowserRouter>
);
export default Routes;
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
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;
Good evening everyone,
I have been trying to add withRouter to my react app so it does not break because of the connect function (see code below).
My code is working, but when i add withRouter to the line below, it breaks my app with the following message :
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App));
Error: Invariant failed: You should not use <withRouter(Connect(App)) /> outside a Router>
i found this topic : Invariant failed: You should not use <Route> outside a <Router> but it's not helping me with me issue when i try to replace with a single Router
Please find below the whole code used :
App.js
import React, {useEffect}from 'react';
import { BrowserRouter } from 'react-router-dom'
import { Route, Redirect} from 'react-router-dom'
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
import * as actions from './store/actions/index'
// Composants
import Layout from './components/hoc/Layout/Layout'
import BudgetHandler from './components/BudgetHandler/BudgetHandler'
import DashBoard from './components/DashBoard/DashBoard'
import Movements from './components/Movements/Movements'
import Home from './components/Home/Home'
import Logout from './components/Logout/Logout'
import classes from './App.module.css'
const App = (props) => {
useEffect(() => {
props.onTryAutoSignup()
},[])
let routes = <React.Fragment>
<Route path="/" exact component={Home} />
<Redirect to="/" />
</React.Fragment>
if(props.isAuthentificated) {
routes = <React.Fragment>
<Route path="/movements" component={Movements} />
<Route path="/dashboard" component={DashBoard} />
<Route path="/logout" component={Logout} />
<Route path="/handler" component={BudgetHandler} />
<Redirect to="/dashboard" />
</React.Fragment>
}
return (
<div className={classes.App}>
<BrowserRouter>
<Layout>
{routes}
</Layout>
</BrowserRouter>
</div>
);
}
const mapStateToProps = state => {
return {
isAuthentificated: state.auth.token !== null
}
}
const mapDispatchToProps = dispatch => {
return {
onTryAutoSignup: () => dispatch(actions.authCheckState())
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App));
And this is happening because i am trying to add this function to the useEffect hook to check permanently if the user is auth or not :
in actions/auth.js
export const authCheckState = () => {
return dispatch => {
const token = localStorage.getItem('token')
if(!token) {
dispatch(logout())
} else {
const expirationTime = new Date(localStorage.getItem('expirationDate'))
const userId = localStorage.getItem('userId')
if(expirationTime > new Date()){
dispatch(logout())
} else {
dispatch(finalSignIn(token, userId))
dispatch(checkAuthTimeout(expirationTime.getSeconds() - new Date().getSeconds()))
}
}
}
}
Thank you for your help
Have a good evening
withRouter can only be used in children components of element. In your case can be used with Movements, DashBoard and other childrens. use it while exporting Movements like
export default withRouter(Movements)
on Movements page.
I have a hypothetic React component named Component.ts with a code structure similar to that:
import React, { FC } from 'react';
import { useParams } from 'react-router-dom';
export const Guide: FC<{}> = () => {
const { serviceId } = useParams();
return (
<p>{serviceId}</p>
)
}
In order to test the behaviour of this component I was trying to do something similar to the following in my Component.test.ts file:
import React from 'react';
import { render } from '#testing-library/react';
import { Component } from './Component';
import { Route, MemoryRouter } from 'react-router-dom';
test('my Component test', async () => {
const someServiceId = 'some-service-id';
const { findByText } = render(
<MemoryRouter initialEntries={[`guides/${someServiceId}`]}>
<Route path='guides/:serviceId'>
<Guide />
</Route>
</MemoryRouter>
);
expect(await findByText(someServiceId)).toBeInTheDocument();
});
My tests were working when I was using version 5.2.2 of react-router-dom but it is failing since I updated to 6.0.0-beta.0. Jest is showing up the following message when running the tests:
Relative pathnames are not supported in createMemoryHistory({ initialEntries }) (invalid entry: "guides/some-service-id")
After much searching, I found the answer in the test suite.
The trick in 6.0.0-beta.0 is to wrap your <Route />'s in <Routes />. Also note the components are imported from react-router instead of react-router-dom:
import {
MemoryRouter,
Routes,
Route,
} from 'react-router';
test('my Component test', async () => {
const someServiceId = 'some-service-id';
const { findByText } = render(
<MemoryRouter initialEntries={[`guides/${someServiceId}`]}>
<Routes>
<Route path='guides/:serviceId'>
<Guide />
</Route>
</Routes>
</MemoryRouter>
);
await waitFor(() => expect(findByText(someServiceId)).toBeInTheDocument());
});
this worked for me
import WarehouseDetailsComponent from '../../containers/WarehouseDetailsPage'
import { render} from '#testing-library/react'
import { Route, MemoryRouter, Routes } from 'react-router-dom'
describe('WarehouseDetails', () => {
test('should display warehouse and user details', async () => {
render(
<MemoryRouter initialEntries={['explore/1234']}>
<Routes>
<Route path="/explore/:id" element={<WarehouseDetailsComponent />} />
</Routes>
</MemoryRouter>
)
// assertions
})
MemoryRouter didn't worked for me
I was receiving this as warn:
Relative pathnames are not supported in createMemoryHistory({ initialEntries }) (invalid entry: "my-route/15")
No routes matched location "my-route/15"
So I figured it as a good option and worked for me.
import { render, screen, waitFor } from '#testing-library/react';
import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from '../../Redux/store';
import MyComponent from './index';
describe('My component test', () => {
it('should access right route', async () => {
render(
<Provider store={store}>
<Router>
<Routes>
<Route path="/" element={<Navigate to="/my-route/15" replace />} />
<Route path="/my-route/:id" element={<MyComponent />} />
</Routes>
</Router>
</Provider>,
);
await waitFor(() => {
expect(screen.getByText('Something should be rendered')).toBeInTheDocument();
});
});
});
MemoryRouter worked for me. See the example below.
import { cleanup, render, screen } from "#testing-library/react"
import { QueryClient, QueryClientProvider } from "react-query";
import Card from "../../../components/Card"
import { MemoryRouter } from "react-router-dom";
import { Route } from "react-router-dom";
import { create } from "react-test-renderer";
describe("test card component", () => {
afterEach(() => {
cleanup();
});
const mockItem = {
name: "Test",
summary: "This is test summary",
}
const MockComponent = () => {
const queryClient = new QueryClient();
const id = '12';
return (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[`post/${id}`]}>
<Route path='post/:id'>
<Card data={mockItem} />
</Route>
</MemoryRouter>
</QueryClientProvider>
)
}
const setup = () => {
return render(<MockComponent />);
}
it("should render the component", () => {
setup()
const cardElement = screen.getByTestId('card')
expect(cardElement).toBeInTheDocument();
expect(cardElement).toHaveTextContent(mockItem.name)
expect(cardElement).toHaveTextContent(mockItem.summary)
})
it("should match the snapshot", () => {
const tree = create(<MockComponent />).toJSON();
expect(tree).toMatchSnapshot();
})
})