Hi there! 馃憢
I am currently implementing Reach Router for the first time (it's rad!) and was trying to find some examples of ways to implement authentication/private routes and couldn't really find anything. So, first, I wanted to ask: am I might totally be missing a cache of examples somewhere?
Assuming I did a decent job at searching and the answer is "no", I would love to contribute some examples to the docs and I wanted to know if that would be welcome/helpful.
Thanks!
That would be great! I'm also looking how to accomplish private routes based on authentication.
Thanks!
I am also interested on how to do private routes.
My simple protected routes(menu) solution with redux
import React from 'react'
import {connect} from 'react-redux'
/**
* @param props {object}
* @returns {React$Element}
*/
function protectedLink (props) {
const {isLogged, children, shared} = props;
return (
<React.Fragment>
{isLogged ? children : shared}
</React.Fragment>
)
}
const mapStateToProps = (state) => {
const {isLogged} = state.user
return {
isLogged,
}
}
export const ProtectedLink = connect(
mapStateToProps,
)(protectedLink)
import React from 'react'
import {Link, LocationProvider} from '@reach/router'
import {ProtectedLink} from './routes/ProtectedLink'
const app = (props) => {
return (
<div>
<LocationProvider history={reachHistory}>
<Link to="/">Main</Link> |{' '}
<ProtectedLink shared={<Link to="enter">Enter</Link>}>
<Link to="feed">Feed</Link> |{' '}
<Link to="create">Create</Link> |{' '}
<Link to="logout" onClick={props.logout}>Logout</Link>
</ProtectedLink>
<RouterBinding/>
</LocationProvider>
</div>
)
}
@kldkv Thanks for the solution.
FWIW, and it will depend on your use case, but I have an Auth component which checks whether the user is authenticated or not. If authenticated, an authenticated layout component is returned which has an internal router. You could have different logged in and logged out Routers.
@niccai could you provide the example?
I ended up doing something similar to what @kldkv proposed:
export default function Main({ children }) {
const { isAuthenticated, updated } = useMappedState(mapState);
if (isAuthenticated) {
return (
<div className="h-screen w-screen flex flex-col md:flex-row bg-grey-lighter">
{children}
</div>
);
}
// Render the login right here!
return (
<Full>
<Login />
</Full>
);
}
I'm rendering the login component instead of the actual content in case the user is not logged in. As soon as the user logs in, the private content gets rendered. No redirects 馃檶
Here's the full implementation:
https://github.com/crysfel/budgeting-app/blob/master/src/containers/Layouts/Main.js#L23
@rubenbase Similar to what is above in the two examples. The main difference for me is that I have two components <Layout /> and <LayoutAuthenticated />. My <Authentication /> component does the check and shows either of those two layouts. It allows me to have different page layouts, and it also allows me to isolate the router component inside <LayoutAuthenticated />.
I'm using a pattern taken from React Router v4, combined with an authentication context component. It seems to work fine in Reach too.
const ProtectedRoute = ({ component: Component, ...rest }) => (
<AuthConsumer>
{({ isAuth }) =>
isAuth ? <Component {...rest} /> : <Redirect from="" to="login" noThrow />
}
</AuthConsumer>
);
const PublicRoute = ({ component: Component, ...rest }) => (
<Component {...rest} />
);
Then you render your routes like this:
<Router>
<PublicRoute path="/login" component={Login} />
<PublicRoute path="/signup" component={Signup} />
<ProtectedRoute path="/" component={Dashboard} />
<ProtectedRoute path="/dashboard" component={Dashboard} />
<PublicRoute default component={NotFound} />
</Router>
You could wrap <Component /> declarations with a Layout component if you're looking to render different layouts on your private/public routes.
So I've been playing around with a solution to this. I wanted to do it in a way which kept the nice Reach Router syntax of just putting props on the page components themselves, and not embedding them as props of Route components or nesting them in SignedIn / SignedOut blocks etc.
I imaged something like just adding a only="signed-in" or only="signed-out" prop on the component, similar to the path prop, and have it just work.
I managed to get there using a HOC on my Page components, and Typescript. It's a problem that you have to remember to wrap the Page in the HOC in addition to adding the prop -- if you just add the prop, without the HOC, nothing will happen, so you could think you have a protected route when in fact you don't, which is a problem -- but Typescript saves the day here with type errors on the props if you do that.
Here's what I have:
type Only = 'signed-in' | 'signed-out'
type WithAuthProps = { only: Only, redirect?: string }
type Props = RouteComponentProps & WithAuthProps & ReturnType<typeof mapState>
type State = { redirect?: string }
class WithAuthPresentation extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
if (props.only === 'signed-in') {
this.state = { redirect: props.signedIn ? undefined : props.redirect }
} else if (props.only === 'signed-out') {
this.state = { redirect: props.signedIn ? props.redirect : undefined }
} else {
this.state = {}
}
}
render() {
return this.state.redirect ? (
<Redirect to={this.state.redirect} />
) : (
<>{this.props.children}</>
)
}
}
const mapState = (state: any) => ({ signedIn: <get signedIn from state> })
const WithAuth = connect(mapState)(WithAuthPresentation)
const defaultRedirectFor = (only: Only) =>
only === 'signed-in' ? '/' : '/dashboard'
const withAuth = (Wrapped: React.ComponentType<RouteComponentProps>) => (
props: RouteComponentProps & WithAuthProps,
) => {
const { only, redirect: redirectProp, ...passThrough } = props
// allow a redirect prop to be passed to the route, else just use default
const redirect = redirectProp || defaultRedirectFor(only)
return (
<WithAuth only={only} redirect={redirect}>
<Wrapped {...passThrough} />
</WithAuth>
)
}
// on pages where you want to use the only="signed-in/signed-out" prop just wrap them with withAuth
const Landing = withAuth(() => <div>landing page</div>)
const SignIn = withAuth(() => <div>sign in...</div>)
const Dashboard = withAuth(() => <div>dashboard, you are signed in!</div>)
// on a public page, just don't use withAuth
const SomePublicPage = () => <div>public content</div>
const Routes: React.FC = () => (
<Router>
<Landing path="/" only="signed-out" />
<SignIn path="/sign-in" only="signed-out" />
<Dashboard path="/dashboard" only="signed-in" redirect='/sign-in' />
<SomePublicPage path="/public-page" />
</Router>
)
To illustrate how Typescript helps, suppose I add a new page SuperSecret
const SuperSecret = () => <div>you need to be signed in to see this!<div>
Notice I forgot to wrap it with withAuth - so I then specify it as only="signed-in" in the routes
const Routes: React.FC = () => (
<Router>
...
<SuperSecret path="/super-secret" only="signed-in" /> <-- TS ERRORS HERE
...
</Router>
)
Typescript will not allow me to pass the only prop, since SuperSecret is not wrapped in withAuth.
Note this is a work in progress and experimental but it's working nicely for me. Couple of notes if you want to implement this yourself:
The types on withAuth only allow for passing RouteComponentProps through to the page - so you can't use the Reach Router feature of being to pass arbitrary extra props at the route level. I don't need this, so have just kept it simple, but if you do you'll have to figure out how to make that work with the types. One really simple solution would be to just pass through all props including only and redirect, I wanted to avoid that just to keep it as clean as possible, but it probably wouldn't cause many problems.
The above implementation works in constructor on purpose, so the checks only happen on route changes. You could change this if you want so it's reactive to state changes, but I have found issues with that - for example on a sign in page, you may want to run the sign in action which updates signedIn state, then navigate somewhere custom, but if the reactive redirect happens first in the above example you would always navigate to the redirect statically defined on the route as soon as you're signed in.
Hope this is useful for someone! I'm pretty happy using it.
const AuthHOC = ( props ) => {
return props.userDetails.id? props.yes() : props.no()
}
<Router >
<Home path = '/'/>
<AuthHOC path = 'protectedPath'
yes ={ () =>
<ProtectedPage/>
}
no = {() =>
<Login/>
}
/>
</Router >
above solution using HOC can be implemented.
I'm using a pattern taken from React Router v4, combined with an authentication context component. It seems to work fine in Reach too.
const ProtectedRoute = ({ component: Component, ...rest }) => ( <AuthConsumer> {({ isAuth }) => isAuth ? <Component {...rest} /> : <Redirect from="" to="login" noThrow /> } </AuthConsumer> ); const PublicRoute = ({ component: Component, ...rest }) => ( <Component {...rest} /> );Then you render your routes like this:
<Router> <PublicRoute path="/login" component={Login} /> <PublicRoute path="/signup" component={Signup} /> <ProtectedRoute path="/" component={Dashboard} /> <ProtectedRoute path="/dashboard" component={Dashboard} /> <PublicRoute default component={NotFound} /> </Router>You could wrap
<Component />declarations with a Layout component if you're looking to render different layouts on your private/public routes.
How do we redirect back user on intended Route using Redirect for /login ?
If someone can help in this case that would be great ?
<Redirect to={{
pathname: '/login',
state: { from: props.location }
}} />
This works in react-router-dom, I am not sure why not in reach-router ?
My solution:
export const protectedComponent = Component => props => {
const [authToken] = useContext(AuthContext);
if (!authToken) return <Redirect from="" to="login" noThrow />;
return <Component {...props} />;
};
You can use like:
const Dashboard = protectedComponent(props => {
return (
<section className="dashboard">
<Navigation />
<main id="main">{props.children}</main>
</section>
);
});
<Router id="router">
<Login path="login" />
<Dashboard path="/">
<JobList path="/" />
<JobCreate path="create" />
</Dashboard>
</Router>
@Ernjivi look at this :-)
Here's an idea.
Have a callback at the Router level. If a component has the prop private or loginRequired, and that route is hit, run the onPrivateRoute or onLoginRequired callback.
If any of you like this syntax, I made a PR here.
const Routes = () => {
const { user } = useAuth()
const handlePrivateRoute = navigate => {
if (!user) navigate('/login')
}
return (
<Router onPrivateRoute={handlePrivateRoute}>
<SettingsPage path="/settings" private />
<LoginPage path="/login" default />
</Router>
);
}
Would be nice if this was a builtin feature.
@alex-cory what does your Router component look like?
Refer to this PR.
Ah nice,
I went a different approach.
const PrivateRoute = ({ as: Component, ...props }) => {
const { user } = useContext(UserContext)
return (
<div>
{user ? <Component {...props}/> : <Login />}
</div>
)
}
export default PrivateRoute
And then in use...
<PrivateRoute as={Dashboard} path="dashboard"/>
I agree, there should be documentation on how to do this.
I see three ways of handling this, and I'm wondering if there are problems or safety concerns with any of the ways:
Are there any pros or cons to either of these approaches?
Based on @dev-drprasad response
here is the HOC that worked for me:
// component/router/AuthenticatedRoute.tsx
export const AuthenticatedRoute = ({ children }: React.ComponentProps<any>) => {
const { userStore } = useStores()
const { authenticated } = userStore
if (!authenticated) return <Redirect to={LOGIN_URL} noThrow />
return children
}
```tsx
// pages/dashboard.tsx
export const DashboardPage = (_props: RouteComponentProps) => {
return (
Dashboard
// yada yada
)
}
```tsx
// app.tsx
export default function App () {
return (
<Router>
<HomePage path={Constants.HOMEPAGE_URL} />
<DashboardPage path={Constants.DASHBOARD_URL} />
<LoginPage path={Constants.LOGIN_URL} />
<NotFound default />
</Router>
)
}
@a14m i really don't like it that way. It's just wrong for me that a page component needs a AuthenticatedRoute component. Router should handle it right before switching to this route.
Like mentioned here: https://github.com/reach/router/pull/332
This is what I've been using whilst working with Typescript:
import { Redirect, RouteComponentProps } from '@reach/router';
import React, { FunctionComponent } from 'react';
type Props = RouteComponentProps & { as: FunctionComponent; isLoggedIn: boolean };
const ProtectedRoute: FunctionComponent<Props> = ({ as: Component, ...props }) => {
const { ...rest } = props;
return props.isLoggedIn ? <Component {...rest} /> : <Redirect from="" to="/login" noThrow />;
};
export { ProtectedRoute };
Regardless, I agree with @vertic4l - wish there is a cleaner way.
This is what I've been using whilst working with Typescript:
import { Redirect, RouteComponentProps } from '@reach/router'; import React, { FunctionComponent } from 'react'; type Props = RouteComponentProps & { as: FunctionComponent; isLoggedIn: boolean }; const ProtectedRoute: FunctionComponent<Props> = ({ as: Component, ...props }) => { const { ...rest } = props; return props.isLoggedIn ? <Component {...rest} /> : <Redirect from="" to="/login" noThrow />; }; export { ProtectedRoute };Regardless, I agree with @vertic4l - wish there is a cleaner way.
Most helpful comment
I'm using a pattern taken from React Router v4, combined with an authentication context component. It seems to work fine in Reach too.
Then you render your routes like this:
You could wrap
<Component />declarations with a Layout component if you're looking to render different layouts on your private/public routes.