I would like to make use of react-router's onEnter
handler in order to prompt users to authenticate when entering a restricted route.
So far my routes.js
file looks something like this:
import React from 'react'; import { Route, IndexRoute } from 'react-router'; export default ( <Route path="/" component={App}> <IndexRoute component={Landing} /> <Route path="learn" component={Learn} /> <Route path="about" component={About} /> <Route path="downloads" component={Downloads} onEnter={requireAuth} /> </Route> )
Ideally, I'd like my requireAuth
function to be a redux action that has access to the store and current state, that works like this: store.dispatch(requireAuth())
.
Unfortunately I don't have access to the store in this file. I don't think I can use really use connect
in this case to access the relevant actions that I want. I also can't just import store
from the file where the store is created, as this is undefined when the app first loads.
Connected React Router is a Redux binding for React Router v4 and v5. It synchronizes router state with Redux store via a unidirectional flow and uses react-hot-loader to facilitate hot reloading of functional components while preserving state.
You can use the connected-react-router library (formerly known as react-router-redux ). Their Github Repo details the steps for the integration. Once the setup is complete, you can now access the router state directly within Redux as well as dispatch actions to modify the router state within Redux actions.
It's simple to get access to the store inside a React component – no need to pass the store as a prop or import it, just use the connect function from React Redux, and supply a mapStateToProps function that pulls out the data you need. Then, inside the component, you can pass that data to a function that needs it.
The easiest way to accomplish this is to pass your store to a function that returns your routes (rather than return your routes directly). This way you can access the store in onEnter
and other react router methods.
So for your routes:
import React from 'react'; import { Route, IndexRoute } from 'react-router'; export const getRoutes = (store) => ( const authRequired = (nextState, replaceState) => { // Now you can access the store object here. const state = store.getState(); if (!state.user.isAuthenticated) { // Not authenticated, redirect to login. replaceState({ nextPathname: nextState.location.pathname }, '/login'); } }; return ( <Route path="/" component={App}> <IndexRoute component={Landing} /> <Route path="learn" component={Learn} /> <Route path="about" component={About} /> <Route path="downloads" component={Downloads} onEnter={authRequired} /> </Route> ); )
Then update your main component to call the getRoutes
function, passing in the store:
<Provider store={ store }> <Router history={ history }> { getRoutes(store) } </Router> </Provider>
As for dispatching an action from requireAuth
, you could write your function like this:
const authRequired = (nextState, replaceState, callback) => { store.dispatch(requireAuth()) // Assume this action returns a promise .then(() => { const state = store.getState(); if (!state.user.isAuthenticated) { // Not authenticated, redirect to login. replaceState({ nextPathname: nextState.location.pathname }, '/login'); } // All ok callback(); }); };
Hope this helps.
If you want that you could write route.js like this:
var requireAuth = (store, nextState, replace) => { console.log("store: ", store); //now you have access to the store in the onEnter hook! } export default (store) => { return ( <Route path="/" component={App}> <IndexRoute component={Landing} /> <Route path="learn" component={Learn} /> <Route path="about" component={About} /> <Route path="downloads" component={Downloads} onEnter={requireAuth.bind(this, store)} /> </Route> ); );
I've setup an example which you could play with in this codepen.
Not sure if triggering an action in order to handle the auth is a good idea. Personally I prefer handling auth in a different way:
Instead of using an onEnter
hook, I use a wrapping function. I want the admin section of my blog protected, therefore I wrapped the AdminContainer
component in the routes with a function, requireAuthentication
, see below.
export default (store, history) => { return ( <Router history={history}> <Route path="/" component={App}> { /* Home (main) route */ } <IndexRoute component={HomeContainer}/> <Route path="post/:slug" component={PostPage}/> { /* <Route path="*" component={NotFound} status={404} /> */ } </Route> <Route path="/admin" component={requireAuthentication(AdminContainer)}> <IndexRoute component={PostList}/> <Route path=":slug/edit" component={PostEditor}/> <Route path="add" component={PostEditor}/> </Route> <Route path="/login" component={Login}/> </Router> ); };
requireAuthentication
is a function that
Login
You can see it below:
export default function requireAuthentication(Component) { class AuthenticatedComponent extends React.Component { componentWillMount () { this.checkAuth(); } componentWillReceiveProps (nextProps) { this.checkAuth(); } checkAuth () { if (!this.props.isAuthenticated) { let redirectAfterLogin = this.props.location.pathname; this.context.router.replace({pathname: '/login', state: {redirectAfterLogin: redirectAfterLogin}}); } } render () { return ( <div> {this.props.isAuthenticated === true ? <Component {...this.props}/> : null } </div> ) } } const mapStateToProps = (state) => ({ isAuthenticated: state.blog.get('isAuthenticated') }); AuthenticatedComponent.contextTypes = { router: React.PropTypes.object.isRequired }; return connect(mapStateToProps)(AuthenticatedComponent); }
Also, requireAuthentication
will protect all routes under /admin
. And you can reuse it wherever you like.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With