Closed fortunebubble closed 8 years ago
In the specific case of authentication/ access handling, I do access the store object directly from the router. I am using onEnter
hook to determine if user can access the content, e.g.
import React from 'react';
import {
Route,
IndexRoute
} from 'react-router';
import {
HomeView,
LoginView,
LogoutView,
ProjectView,
ProjectIndexView
} from './../views';
import store from './../store';
let requireAuthentication;
requireAuthentication = (nextState, replace) => {
let isAuthenticated;
isAuthenticated = store.getState().getIn(['authentication', 'isAuthenticated']);
if (!isAuthenticated) {
replace('/authentication/login');
}
};
export default <Route path='/'>
<IndexRoute component={HomeView} onEnter={requireAuthentication} />
<Route path='/authentication/login' component={LoginView} />
<Route path='/authentication/logout'component={LogoutView} />
<Route path='/' onEnter={requireAuthentication}>
<Route path='/projects' component={ProjectIndexView} />
<Route path='/project/:projectId' component={ProjectView} />
</Route>
</Route>;
how do I do that?
I am assuming you have ./createStore.js
, which is something like:
import _ from 'lodash';
import {
createStore,
applyMiddleware
} from 'redux';
import createLogger from 'redux-logger';
import thunk from 'redux-thunk';
import {
syncHistory
} from 'react-router-redux';
import {
browserHistory
} from 'react-router';
import Immutable from 'immutable';
import rootReducer from './reducers';
import {
ENVIRONMENT
} from './config';
let defaultInitialState;
defaultInitialState = Immutable.Map();
export default (initialState = defaultInitialState) => {
let createStoreWithMiddleware,
reduxRouterMiddleware,
store;
reduxRouterMiddleware = syncHistory(browserHistory);
if (ENVIRONMENT === 'production') {
createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware, thunk)(createStore);
}
if (ENVIRONMENT === 'development') {
let logger;
logger = createLogger({
collapsed: true,
stateTransformer: (state) => {
return state.toJS();
}
});
createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware, thunk, logger)(createStore);
}
store = createStoreWithMiddleware(rootReducer, initialState);
if (ENVIRONMENT === 'development') {
reduxRouterMiddleware.listenForReplays(store, (state) => {
return state.getIn(['route', 'location']).toJS();
});
}
if (module.hot) {
module.hot.accept('./reducers', () => {
return store.replaceReducer(require('./reducers').default);
});
}
return store;
};
Then you create a second file (to separate implementation from instructions), `./store.js, e.g.
import createStore from './createStore';
export default createStore();
Since Redux app is using a single store, this approach (to the best of my understanding) is perfectly valid.
Since Redux app is using a single store, this approach (to the best of my understanding) is perfectly valid.
This is fine for client-only apps but we don't recommend this approach because it is much harder to add (or experiment with) server rendering if you rely on a singleton store.
Instead, we suggest to explicitly inject store into anything that needs it. For example instead of exporting routes, you could export createRoutes(store)
. This should give you some idea: https://github.com/acdlite/redux-router/issues/60#issuecomment-141691675
I tried this approach with injecting store into router config, but I did not require/import the store, rather I exported a router config factory function which took the store as an argument. I discovered having the authentication logic in the routing config to be quite clunky.
I came up with a different, more redux-way approach, not using route hooks for authentication. I still inject the store into router config for cases where I'd need to dispatch some action from route hooks regardless of the rendered view (like logout by visiting a /logout route).
I've implemented an authenticated
decorator which wraps a component that needs authentication with a higher-level component which connects to the auth
and to the routing
stores, and dispatches a routing action if a redirect is required. This allows me to require authentication from any view, so if a view that requires authentication is rendered, the authentication gets checked.
import React, { PropTypes, Component } from 'react';
import { connect } from 'react-redux';
import { routeActions } from 'react-router-redux';
import {
extractState as extractAuthState,
isAuthenticated,
} from 'redux/reducers/auth';
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
export default function authDecoratorFactory(componentDoesNotRequireAuthentication) {
// Use inverse logic to have `@authenticated()` for components that require authentication.
const componentRequiresAuthentication = !componentDoesNotRequireAuthentication;
// TODO: Move these URLs to the decorator options or global config.
const urls = {
preLoginDefault: '/',
logInDefault: '/',
loginForm: '/login',
};
return function authDecorator(WrappedComponent) {
class AuthenticatedComponentImpl extends Component {
static propTypes = {
auth: PropTypes.object.isRequired,
location: PropTypes.object,
dispatch: PropTypes.func.isRequired,
}
componentDidMount() {
this._redirectIfNeeded(this.props);
}
componentWillReceiveProps(nextProps) {
this._redirectIfNeeded(nextProps);
}
_redirect(redirectPathnameAndQueryString, currentPathnameAndQueryString) {
if (redirectPathnameAndQueryString === currentPathnameAndQueryString) {
// Avoid infinite redirect.
return;
}
this.props.dispatch(routeActions.push(redirectPathnameAndQueryString));
}
_redirectIfNeeded(nextProps) {
if (!nextProps.location) {
return;
}
const currentPathnameAndQueryString = nextProps.location.pathname;
const currentPathnameAndQueryParsed = require('url').parse(currentPathnameAndQueryString);
const currentPathname = currentPathnameAndQueryParsed.pathname;
const currentQueryString = currentPathnameAndQueryParsed.query;
const currentQuery = require('qs').parse(currentQueryString || '');
if (!isAuthenticated(this.props.auth) && isAuthenticated(nextProps.auth)) {
// Became authenticated, redirect to post-login section.
this._redirect(currentQuery && currentQuery.next || urls.logInDefault, currentPathnameAndQueryString);
}
else if (isAuthenticated(this.props.auth) && !isAuthenticated(nextProps.auth)) {
// Became non-authenticated, redirect to pre-login section.
this._redirect(urls.preLoginDefault, currentPathnameAndQueryString);
}
else if (componentRequiresAuthentication && !isAuthenticated(nextProps.auth)) {
// Should not be on a page with this component, redirect to login page.
if (currentPathname === urls.loginForm) {
// Avoid infinite redirect.
return;
}
const redirectQuery = {};
if (nextProps.location.pathname && nextProps.location.pathname !== urls.logInDefault) {
redirectQuery.next = nextProps.location.pathname;
}
const redirectQueryString = require('qs').stringify(redirectQuery);
this._redirect(urls.loginForm + (redirectQueryString ? '?' + redirectQueryString : ''));
}
else if (isAuthenticated(nextProps.auth)) {
// Should not be on the login page, redirect to post-login section.
if (currentPathname === urls.loginForm) {
this._redirect(currentQuery && currentQuery.next || urls.logInDefault);
}
}
}
render() {
if (componentRequiresAuthentication) {
if (!isAuthenticated(this.props.auth)) {
return null;
}
}
return (
<WrappedComponent {...this.props} />
);
}
}
const AuthenticatedComponent = connect(
(state) => ({
auth: extractAuthState(state),
location: state.routing.location,
})
)(AuthenticatedComponentImpl);
AuthenticatedComponent.displayName = 'AuthenticatedComponent(' + getDisplayName(WrappedComponent) + ')';
return AuthenticatedComponent;
};
}
Example usage:
// routes.js
export default (store) => {
return (
<Route path="/" component={App}>
<Route component={PostLoginLayout}>
</Route>
<Route component={PreLoginLayout}>
<Route path="login" component={LoginPage} />
<Route path="logout" onEnter={() => {
store.dispatch(logOut());
}} />
</Route>
</Route>
);
};
// The authentication is not required, but we'd like
// to get redirected from this view to a post-login experience
// upon getting authenticated.
@authenticated(true)
export default class PreLoginLayout extends Component {
// The authentication is required, and we'd like
// to get redirected to the login form if we're not authenticated.
@authenticated()
export default class PostLoginLayout extends Component {
Thank you all for sharing
An implementation of the approach I proposed above as a more configurable higher-order component has popped up in a sibling thread about authentication -- @fortunebubble probably would be interested: https://github.com/mjrussell/redux-auth-wrapper
I am thinking to redirect a user to homepage after he/she successfully login. My question is should I do access the login user info(in redux store) in the route level? If yes, how do I do that? or should I access the info within the homepage component using the
connect
from react-redux