In Gatsby, how would I restrict routes programmatically? Using react-router, I see it's possible to do a <Redirect> with <Route>, but how would this be implemented in Gatsby? To do something like this...
<Route exact path="/" render={() => (
loggedIn ? (
<Redirect to="/dashboard"/>
) : (
<PublicHomePage/>
)
)}/>
Where would I put this file in Gatsby? Would I put it in src/pages or elsewhere?
Edited, asking for additional clarification...
I'm able to get this work per the advice from #Nenu and the Gatsby docs. The docs gave a non-asynchronous example, so I had to tweak it for interacting with a remote server like this...
async handleSubmit(event) {
event.preventDefault()
await handleLogin(this.state)
.then(response => _this.setState({isLoggedIn: isLoggedIn()}))
.catch(err => { console.log(err) });
}
Also, I am able to use the <PrivateRoute /> with this.
Unfortunately though, when I render using...
render() {
if (isLoggedIn()) {
return <Redirect to={{ pathname: `/app/profile` }} />
}
return (
<View title="Log In">
<Form
handleUpdate={e => this.handleUpdate(e)}
handleSubmit={e => this.handleSubmit(e)}
/>
</View>
)
}
...while I do indeed <Redirect to={{ pathname:/app/profile}} />, I notice that a split-second before I redirect, the form fields are emptied and only after that do I get redirected to /app/profile (from /app/login). Also, if I type in an incorrect password, my whole form is re-rendered (re-rendering <View /> again). This would be a bad user-experience because they they'd have to re-enter all of their info from scratch, plus I'd be unable to add styling for invalid inputs, etc. I'm wondering if there is a better way to do this with Gatsby.
Or, would I have to build form functionality more from scratch (i.e., using Redux, Router, etc more directly) rather than depending upon Gatsby's higher level of abstraction?
Gatsby uses react-router under the hood, therefore you can define your client-only routes with it.
There is, as always with gatsby, a very nice example in the github repo:
https://github.com/gatsbyjs/gatsby/tree/master/examples/simple-auth
And the doc about it:
https://www.gatsbyjs.org/docs/building-apps-with-gatsby/#client-only-routes--user-authentication
To sum up, this is what is done:
1) Create a PrivateRoute component in /src/components
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
!isLoggedIn() ? (
// If we’re not logged in, redirect to the login page.
<Redirect to={{ pathname: `/app/login` }} />
) : (
<Component {...props} />
)
}
/>
);
2) Define routes in your wanted prefix "client-only" path
Let suppose you want to restrict access of the /app/:path section of your website, then in /src/pages/app.js:
const App = () => (
<div>
<PrivateRoute path="/app/profile" component={Home} />
<PrivateRoute path="/app/details" component={Details} />
<Route path="/app/login" component={Login} />
</div>
);
These routes will exist on the client only and will not correspond to index.html files in an app’s built assets. If you wish people to visit client routes directly, you’ll need to setup your server to handle these correctly. (source)
3) White-list the client-routes in gatsby-node.js
exports.onCreatePage = async ({ page, boundActionCreators }) => {
const { createPage } = boundActionCreators
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/app/)) {
page.matchPath = `/app/:path`
// Update the page.
createPage(page)
}
}
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 am currently in the process of creating a react webpage, using starlette as my web server framework that hooks up my database and provides the API. To improve code separation and unneeded loading of files I divided my page into two separately built react pages. One for the login page before verification, and one for the main page once verification has been completed and the user has a valid token. The problem with this is that both react web pages send GET requests as an example to: /static/js/2.91da4595.chunk.js.
I am wondering if it is possible for me to change where react will send requests to when looking for static files. So for example my login page will look to /otherstatic/js/2.91da4595.chunk.js instead.
There might be a more elegant way to reach the point I want to, so feel free to suegest a different method. Let me know if any further explanation or code is needed, and I can add it to this post.
You may need to do code-splitting. Read here for further information.
Code-splitting your app can help you “lazy-load” just the things that are currently needed by the user, which can dramatically improve the performance of your app. While you haven’t reduced the overall amount of code in your app, you’ve avoided loading code that the user may never need, and reduced the amount of code needed during the initial load.
I assume you used react-router-dom, so here's a simple implementation:
import React, { Suspense } from 'react';
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
const HomePage = React.lazy(() => import('./HomePage'));
const LoginPage = React.lazy(() => import('./LoginPage'));
function MyApp() {
const [auth, setAuth] = React.useState({
isLoading: true,
isAuthenticated: false,
data: null,
})
React.useEffect(() => {
const checkAuth = () => {
// call setAuth here
}
checkAuth()
}, [])
const MyRoute = ({ component: Component, authorized: false, ...rest }) => (
<Route
{...rest}
render={props => {
if (auth.isLoading) return null
if (authorized) { // Home page access
return auth.isAuthenticated
? <Component {...prop} />
: <Redirect to="/login" />
} else { // Login page access
return !auth.isAuthenticated
? <Component {...prop} />
: <Redirect to="/" />
}
}}
/>
)
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<MyRoute path="/login" component={LoginPage} authorized={false} />
<MyRoute path="/" component={HomePage} authorized={true} />
</Switch>
</Suspense>
</BrowserRouter>
);
}
folks. I'm learning how to integrate React with Express using React Router, and I've run into a problem with authenticating users. I'm trying to use a higher order component to conditionally render a protected route based on a user's authorization status.
const ProtectedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props => {
if (!AUTHORIZED) {
return <Redirect to="/login" />;
}
return <Component {...props} />;
}}
/>
);
};
The problem I'm having is in the if (!AUTHORIZED) statement. I'm using Passport to handle authentication on the Express server side, and I have an endpoint set up for retrieving user information and authorization status, but I can't figure out how to get access to that data before the page renders. If I was using a class component instead of a functional component, (learning hooks also), I think I could get the data with the componentWillMount lifecycle method, but I read that's bad practice. Any ideas on how I could move forward from here would be much appreciated!
***edit***
A couple of things I tried to get this working...
I tried adding an authorization module to fetch the data for me.
class Auth {
constructor() {
this.authenticated = false;
}
async isAuthenticated() {
console.log("hitting auth route");
await fetch("/api/auth")
.then(res => res.json())
.then(json => {
if (json.error) {
this.authenticated = false;
}
this.authenticated = true;
});
return this.authenticated;
}
}
export default new Auth();
I import the module and plug auth.authenticated() in place of the placeholder AUTHORIZED. This function gets skipped, because it's asynchronous, and the redirect will always occur.
So I need to add await to auth.authenticated(). But now I need to have async further up the chain, so I foolishly add async in front of props, as such:
render={async props => {
So now It's trying to render a promise object instead of a component, and we get the error Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
Promises all the way down.
Answering this in case anyone runs into a similar issue...
The first solution I had was based on Firealem Erko's comment. On login, I saved a variable with the user's ID to local storage and referenced that in my component. This was a good first solution, but later was improved by something rotimi-best mentioned in his comment. It turns out you can indeed pass props to these components, which I did not realize in my inexperience. So that is now the way I'm doing it. The final solution is as follows:
const ProtectedRoute = ({
component: Component,
logged,
setLogged,
...rest
}) => {
return (
<Route
{...rest}
render={props => {
if (!logged) {
return (
<Redirect
to={{
pathname: "/login",
state: { flashInfo: "Please log in to continue." }
}}
/>
);
} else {
return <Component {...props} logged={logged} setLogged={setLogged} />;
}
}}
/>
);
};
And here's the parent component where I'm passing in the props:
function App() {
let [logged, setLogged] = useState(false);
useEffect(() => {
if (window.localStorage.getItem("qrs")) {
setLogged(true);
} else {
setLogged(false);
}
}, []);
return (
<div className="App">
<BrowserRouter>
<Nav logged={logged} setLogged={setLogged} />
<Switch>
<ProtectedRoute
exact
path="/dashboard"
component={Dashboard}
logged={logged}
setLogged={setLogged}
/>
<Route
path="/register"
exact
render={props => <Register {...props} logged={logged} />}
/>
<Route
path="/login"
exact
render={props => (
<Login {...props} logged={logged} setLogged={setLogged} />
)}
/>
</Switch>
</BrowserRouter>
</div>
);
}
Thanks to all the commenters for their suggestions!
I am building a full stack application with React and Spring Boot, but came across one problem that I didn't find an answer anywhere.
The behavior that I am trying to achieve is as follows:
If a user wants to visit login or register page, I want to redirect user to the home page if they are authenticated. The isAuthenticated is a flag in redux that I set when the app first loads.
I also want to redirect user to the login page if they are not authenticated and tries to visit home page.
The first goal works fine as there are plenty of tutorials out there explaining how to use react-router to achieve that, but the first goal seems not to work properly in production.
I have a Spring Boot application running as a REST API on the backend, and I am using eirslett/frontend-maven-plugin to package both my front and backend into a single jar for deployment.
When I spin-up both front and backend separately as two development servers locally, the app will redirect me to the login page if I am not logged in (if I try to access secured routes by react-router). However, it will give me an error 401 page if I went directly to the /login route when not authenticated, but it will not give me error if I try to visit the home route and gets redirected to /login. Therefore, even if I got redirected to the /login just fine, I can't refresh the page or it will give me error 401.
The root component of my React front end looks like this, which utilizes React-router:
function App() {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<div className='AppContainer'>
<Navbar />
<Switch>
<PublicRoute path='/login' component={Login} />
<PublicRoute path='/register' component={Register} />
<PrivateRoute path='/' component={Todos} />
</Switch>
</div>
</ConnectedRouter>
</Provider>
);
}
export default App;
The PublicRoute and PrivateRoute component that I used looks as follow:
function PrivateRoute({ component: Component, isAuthenticated, ...rest }) {
return (
<Route
{...rest}
render={props =>
isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: '/login', state: { from: props.location } }}
/>
)
}
></Route>
);
}
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect(mapStateToProps)(PrivateRoute);
function PublicRoute({ component: Component, isAuthenticated, ...rest }) {
return (
<Route
{...rest}
render={props =>
isAuthenticated ? (
<Redirect to={{ pathname: '/', state: { from: props.location } }} />
) : (
<Component {...props} />
)
}
></Route>
);
}
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect(mapStateToProps)(PublicRoute);
Does anyone have any idea what is going on? I feel like the problem exists because of the way I configured my PublicRoute component and Spring Security, but I don't know how to make them work together. Or, to my naive understanding, my frontend application will only load if I first visit my root route therefore it will redirect me to /login just fine; but if the first page I visit is /login, the root component of React will not load and therefore this route is secured by my Spring Security configuration. But is there a way to get around with that?
Any help is appreciated!
I figured it out by realizing that I am using BrowserRouter instead of a HashRouter, and that is why I am getting 401 and 404. The react-router documentation actually provides a great explanation on why should use a HashRouter here.
i am working on a huge project and i have an authorization system where Logged in users can access certain routes while the Not Found component, served on Logged Out Users. The problem is, when a user is marked as logged in and has a valid token and then proceed into a page (e.g /account), the component Not Found is rendered first and a second later the component account is rendered correctly.
I have an async componentDidMount where i validate my token and if it's valid, i set the state of isLogged: true. Also on each route in my switch i am checking the the state of isLogged and only then i send the corresponding component.
My async componentDidMount
{/* Username is from localstorage */}
async componentDidMount() {
if ((USERNAME != "") && (USERNAME != null)) {
const logged = await checkTheToken({'Username': USERNAME}).then((result) => {
if(result.status == 202) {
this.setState({isLogged: true});
this.setState({checking: false});
};
});
}
}
This is the Router (* supposing we are trying to access /user page. Notice i use react-router, react-router-dom ^4.1.1* )
if (this.state.checking) {
return null;
} else {
return (
<Router>
<Switch>
{/* This is Not Rendered First and Not Found rendered before FALSLY */}
{this.state.isLogged && <Route exact path="/user" component={() => (<User isLogged={this.state.isLogged} username={USERNAME} />)} />}
{/* This is Rendered First and Not Found is not rendering at all CORRECTLY */}
<Route exact path="/test" component={() => (<Test isLogged={this.state.isLogged} />)} />
<Route component={() => (<NotFound isLogged={this.state.isLogged} />)} />
</Switch>
</Router>
);
I think the problem is in the {this.state.isLogged && ...} because if we try to access /test the component is rendered correctly without Not Found rendering first.
Also, I tested all the lifecycle methods
I think you're right and the issue comes from {this.state.isLogged && ...}
Lets take it step by step.
First this.state.isLogged is falsy. It does mean that <User isLogged={this.state.isLogged} username={USERNAME} /> is not currently in the ReactRouter configuration.
We can guess that ReactRouter will match the default component (<Route component={() => (<NotFound isLogged={this.state.isLogged} />)} />) since /user is not in its configuration.
The actual behavior is then correct.
The fastest way to archieve your goal would be to move the token check into your child component with something like this :
async componentDidMount() {
if ((USERNAME != "") && (USERNAME != null)) {
const logged = await checkTheToken({'Username': USERNAME}).then((result) => {
if(result.status !== 202) {
history.push('/not-found')
} else {
this.setState({isLogged: true});
this.setState({checking: false});
};
});
}
}
Rendering function would like this :
render () {
if(!this.state.isLogged) return <span />
return <div>...</div>
}
The main problem with this aproach is that it would require all your authenticated components to implement this.
You'll also need to factorize the code into a service to avoid multiple calls.
A 2nd approach would be to fatorize this into a proxy component that do the check like this :
<Router>
<Switch>
<Route component={Authenticated}>
<Route exact path="/user" component={() => (<User isLogged={this.state.isLogged} username={USERNAME} />)} />
</Route>
<Route exact path="/test" component={() => (<Test isLogged={this.state.isLogged} />)} />
<Route component={() => (<NotFound isLogged={this.state.isLogged} />)} />
</Switch>
</Router>
This component will now be the one who carry the token check.
Rendering function would look like this :
render () {
if(!this.state.isLogged) return <span />
return {this.props.children}
}
Main problem here is that you can't easily share between your component the "this.state.isLogged"
If you want to share and update mutiple component using some global state, i highly suggest you to give a look at Redux.
Since redux can have a hard learning curve, if you only need to share this single value, you could try some things using the observable pattern. (Example : observable service, observable service + connector )
All my code example are not tested and are here to guide you in the right direction. In my opinion there is a lot of way to archieve what you want, sadly i can only tell you why it currently do not work and i hope i gave you enough clues to find a solution adapted to your use case.