I am struggling with a re-render in my project. First, I'm checking a cookie before I redirect the user to the login page. I am using Redux Store with this structure.
var initialState = {
user: {
auth: false,
isFetching: false,
isTokenFetching: false
}
}
Fetching is for login via login form, Token fetching is for login via a Token in cookies
I'm using a customized Dynamic route to make it render based on this condition. (isFetching is when token is fetching)
const DynamicRoute = ({isFetching,isAuthenticated, ...rest}) => {
return isFetching ? <h1>Fetching</h1> : (
<Route {...rest} render={(props) =>{
return isAuthenticated ?
<HomePage/> :
<LogInPage/>}}/>
)
}
This function is called before the action is dispatched, So it Renders LogInPage first, and Fetching when action is dispatched, and Render HomePage again when the token is valid.
How can I make sure it only renders the LogInPage and HomePage once.
For example if token is valid (HomePage only), if token invalid (LogInPage only)
I want this dynamic route to work because I hope to use an URL for both condition.
Sorry if my explanation is bad
First, if isAuthenticated is true, we should redirect user to HomePage, shouldn't we?
return isAuthenticated ? <HomePage/> : <LogInPage/>;
How can I make sure it only renders the LogInPage and HomePage once.
Use Redirect component to redirect user back to LogInPage like this:
return isAuthenticated ? <HomePage/> : <Redirect to="/login" />;
You can get the Redirect component from react-router-dom:
import { Redirect } from "react-router-dom";
I assume that you have defined router like this:
<Route path="/login" component={LogInPage} />
IMHO, you should change the way to authenticate your workflow, because it is not good to scale for the future. Have a look at this: react-router auth-worflow example
Basically, your routers will become something like:
<Route path="/login" component={LogInPage} />
<PrivateRoute path="/home" component={HomePage} />
Related
my site is built using MERN stack, when I refresh a page it first shows the main page and then the page where the user is. How to fix this issue?
For example:
if I refresh (/profile) page then for a meanwhile it shows (/) then it redirects to (/profile). I want if I refresh (/profile) it should be on the same page.
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({ component: Component, authed, ...rest }) => {
return (
<Route
{...rest}
render={(props) => authed === true
? <Component {...props} />
: <Redirect to={{ pathname: '/', state: { from: props.location } }} />}
/>
)
}
export default PrivateRoute;
Router code:
const App = () => {
const user = useSelector((state) => state?.auth);
return (
<>
<BrowserRouter>
<Container maxWidth="lg">
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" exact component={About} />
<Route path="/terms" exact component={Terms} />
<PrivateRoute authed={user?.authenticated} path='/profile' component={Profile} />
</Switch>
</Container>
</BrowserRouter>
</>
)
}
export default App;
How to fix so that user stays on the same page if its refreshed? The issue is on the pages where authentication is required.
When first authenticated the user, store the credentials(the info that you evaluate to see if the user is authenticated. Tokens etc.) in the localStorage. Of course you have to create necessary states too.
Then with useEffect hook on every render set the credential state from localStorage.
function YourComponentOrContext(){
const[credentials, setCredentials] = useState(null);
function yourLoginFunction(){
// Get credentials from backend response as response
setCredentials(response);
localStorage.setItem("credentials", response);
}
useEffect(() => {
let storedCredentials = localStorage.getItem("credentials");
if(!storedCredentials) return;
setCredentials(storedCredentials);
});
}
I guess on mounting (=first render) your user variable is empty. Then something asynchronous happen and you receive a new value for it, which leads to new evaluation of {user?.authenticated} resulting in true and causing a redirect to your /profile page.
I must say I'm not familiar with Redux (I see useSelector in your code, so I assume you are using a Redux store), but if you want to avoid such behaviour you need to retrieve the right user value on mounting OR only render route components when you've got it later.
I have reading component which must show only when user is loggedIn. I now redirect the user to /login page if the user is not authenticated. But during the redirect, the reading page is displayed for a few milli seconds allowing the un-authenticated user to see a flickering of the reading page during redirection.
I tried using useLayoutEffect instead of useEffect but the same flickering happens. Also tried without using useEffect or useLayoutEffect and within a function, but still the same
I get the userInfo from a global state, which gets the userInfo from the cookie. For state management I use recoil.
Reading Page: (which must be protected and if no user, redirect to login page)
function index() {
const router = useRouter();
const userInfo = useRecoilValue(userAtom); ---> userInfo is recoil global state
useLayoutEffect(() => {
if (!userInfo) {
router.push("/login?redirect=/reading");
}
}, []);
return (//some code)
Note:
Adding a Loader worked, but is there any better way?
Check the authentication state
show loader based on authentication state
Redirect the user
I would suggest a much better way. Instead of checking on individual pages.
You can write your Auth Check for user at route level i.e inside index.js where the React-Router is defined.
// PrivateRoute.jsx
function PrivateRoute ({component: Component, isAuth, ...rest}) {
return (
<Route
{...rest}
render={(props) => isAuth
? <Component {...props} />
: <Redirect to={{pathname: '/login', state: {userInfo} }} />}
/>
)
}
// index.jsx
.
const userInfo = useRecoilValue(userAtom);
.
<Route path='/login' component={Login} />
<Route path='/register' component={Register} />
<PrivateRoute isAuth={!!userInfo} userInfo={userInfo} path='/dashboard' component={Dashboard} />
.
.
Hope this finds it helpful.
I am trying to redirect to an external URL in a react component.
I am verifying when the component is loaded if it contains an access_token in the local storage. If it does not exist I want to redirect to an auth provider to perform login and then to store the token when it redirects back to me.
However I found that assigning a value to window.location or window.location.href or using window.location.replace(someUrl) it does not work, it simply appends the new url to my application url.
Here is my react component where I am doing this:
class App extends React.Component {
componentDidMount() {
if (!localStorage.getItem('access_token')) {
window.location.replace(someUrl)
}
}
render() {
return (
<HashRouter>
<React.Suspense fallback={loading()}>
<Switch>
<Route path="/" name="Home" render={props => <DefaultLayout {...props}/>}/>
</Switch>
</React.Suspense>
</HashRouter>
)
}
}
Is there a different way on how the redirect to an external URL should be done in React?
I also tried this one and it does not work, it appends the external URL to the one of my application:
React-Router External link
Use React Router Redirect component and provide address in to attribute.
<Redirect push to="/somewhere/else" />
https://reacttraining.com/react-router/web/api/Redirect
I've created a simple React app with Redux, React Router and Auth0 which handles user authentications.
I'm trying to create this basic behavior to control access:
All unauthenticated users will automatically be sent to /public
Authenticated users can access all the other parts of the app
Once a user is authenticated by Auth0, I want to process the access_token and send user to / which is the Home component
Everything is "almost" working the way it should. The problem I'm having is that render() function in App.jsx is executing BEFORE the lock.on('authenticated') listener even has a chance to process the tokens returned by Auth0. As a result, the tokens are never stored and the user always seems to be unauthenticated. If I send user to /login, everything works fine because I'm not checking to see if the user is authenticated before rendering the Login component.
I think the way I'm handling protected routes needs to change. Any suggestions as to how to handle protected routes?
I'm providing the code that you need here. If you want to see the whole app, go to https://github.com/imsam67/react-redux-react-router-auth0-lock
The following is the App.jsx:
class App extends Component {
render() {
const isAuthed = isAuthenticated();
return (
<div>
<Switch>
<Route exact path="/" render={ props => isAuthed ? <Home {...props} /> : <Redirect to="/public" /> } />
<Route exact path="/login">
<Login />
</Route>
<Route path="/public">
<Public />
</Route>
</Switch>
</div>
);
}
}
This is the AuthWrapper component where I handle Auth0:
class AuthWrapper extends Component {
constructor(props) {
super(props);
this.onAuthenticated = this.onAuthenticated.bind(this);
this.lock = new Auth0Lock('my_auth0_client_id', 'my_domain.auth0.com', {
auth: {
audience: 'https://my_backend_api_url',
redirectUrl: 'http://localhost:3000/',
responseType: 'token id_token',
sso: false
}
});
this.onAuthenticated();
}
onAuthenticated() {
debugger; // After successful login, I hit this debugger
this.lock.on('authenticated', (authResult) => {
debugger; // But I never hit this debugger
let expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
sessionStorage.setItem('access_token', authResult.accessToken);
sessionStorage.setItem('id_token', authResult.idToken);
sessionStorage.setItem('expires_at', expiresAt);
});
}
render() {
return(
<AuthContext.Provider value={{ lock: this.lock }}>
{this.props.children}
</AuthContext.Provider>
);
}
}
And here's index.js in case you need to see it:
import App from './components/App';
import AuthWrapper from './components/auth/AuthWrapper';
// Store
import appStore from './store/app-store';
const store = appStore();
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<AuthWrapper>
<App />
</AuthWrapper>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
Here are the changes I made to make this work.
I now initialize Auth0 Lock in index.js to make it global
I moved the onAuthenticated() listener to LoginCallback.jsx component which is where the user is sent after a successful login. I believe moving the onAuthenticated() from App.jsx to LoginCallback.jsx made the biggest impact because the listener in LoginCallback.jsx executes before App.jsx.
On successful authentication, I also use this.props.history.push('/'); to send user to Home component
For full code, please see the repo at https://github.com/imsam67/react-redux-react-router-auth0-lock
Remember nothing is protected in the client side at all. If your concerned of routing to a component without auth just make sure no data is exposed(assuming they can't get any data without a token) and redirect if they landed there even after your router checks for auth or shows an error.
Remember nothing is protected in the client side at all. If your concerned of routing to a component without auth just make sure no data is exposed and redirect if they landed there even after your router checks for auth. I think #Sam has it right. The routes may not respond as expected to an asynchronous call changing it or may have odd behavior. I've never attempted a dynamic route this way but always had conditional renders of content components. A better approach may be to send the call and in the then block redirect to a url which the router knows to handle. Just not sure the router handles this very well. Catch the component checking for auth on load and redirect back to log on if not authorized. Sorry I'm not helping much here but conditional routes almost seem like an anti pattern but I guess it could work if we knew how the router renders its data after changes or if it actually does at all(the routes them selves.) So if they were to bookmark the url and try to return back that would be a 404 right? Maybe like a 401 unauthorized showing and redirect or link to log in might be better?
Dynamic routing need to be defined outside of the <Switch> scope. Here is an exemple assuming your function isAuthenticated() is a state (Redux or wathever)
import React from "react";
import ReactDOM from "react-dom";
import { createBrowserHistory } from "history";
import { Router, Route, Switch, Redirect } from "react-router-dom";
// core components
import Admin from "layouts/Admin.js";
import SignIn from "layouts/SignIn";
const hist = createBrowserHistory();
const loggedRoutes = () => (
<Switch>
<Route path="/" component={SignIn} />
<Route path="/admin" component={Admin} />
<Redirect from="/admin" to="/admin/aboutUs/whatWeDo" />
</Switch>
);
const routes = () => (
<Switch>
<Route path="/" component={SignIn} />
<Route path="/login" component={Admin} />
<Redirect from="/" to="/login" />
</Switch>
);
ReactDOM.render(
<Router history={hist}>
{checkIfAuth? loggedRoutes():routes()}
</Router>,
document.getElementById("root")
);
In this exemple, If you are not login you are redirect to /login.
I am using an App Component to hold all the route so it looks like
class App extends React.Component {
state = { signedIn: false };
handleSignedIn = () => {
this.setState({ signedIn: true });
};
render() {
<Router>
<Route>Header</Route>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/login" component={Login}/>
{...otherRoutes}
</Switch>
<Route>Footer</Route>
</Router>;
}
}
then in one of the routes, I have something like this
<NavLink to="/">
Home
</NavLink>
However, i find that clicking on this NavLink rerenders App and set signedIn to false, what's the right/better way to navigate around this.
-- updated routes for more details
Basically i am expecting the <App> itself not to rerender, but the <Router> should.
Returning to App.js means signedIn is reset to false, since the default state is false. Refreshing the page will also rerender App.js and reset it back to false.
You need some way of checking if the user is logged in already. One way of doing so is by storing a token client-side after logging in, and checking if the token is present when a component loads.
You can possibly store the token in localStorage, and then whenever App.js mounts do something like componentDidMount -> checkToken() -> setState({signedIn: true})