Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it a good idea to access redux store in route? #1336

Closed
fortunebubble opened this issue Jan 31, 2016 · 5 comments
Closed

Is it a good idea to access redux store in route? #1336

fortunebubble opened this issue Jan 31, 2016 · 5 comments
Labels

Comments

@fortunebubble
Copy link

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

@gajus
Copy link
Contributor

gajus commented Jan 31, 2016

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.

@gaearon
Copy link
Contributor

gaearon commented Feb 1, 2016

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: acdlite/redux-router#60 (comment)

@sompylasar
Copy link

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 {

@fortunebubble
Copy link
Author

Thank you all for sharing

@sompylasar
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants