reduxjs / redux

A JS library for predictable global state management
https://redux.js.org
MIT License
60.88k stars 15.27k forks source link

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

Closed fortunebubble closed 8 years ago

fortunebubble commented 8 years ago

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 commented 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.

gaearon commented 8 years ago

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

sompylasar commented 8 years ago

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 commented 8 years ago

Thank you all for sharing

sompylasar commented 8 years ago

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