redux-router icon indicating copy to clipboard operation
redux-router copied to clipboard

Failing unit test demonstrating undefined 'getState' in transition hooks

Open davemaurakis opened this issue 8 years ago • 40 comments

Failing test case to demonstrate that getState throw as error when evaluated in the context of an onEnter transition hook.

davemaurakis avatar Sep 18 '15 17:09 davemaurakis

Thank you! I <3 pull requests with failing tests. I'll get to this very soon.

acdlite avatar Sep 18 '15 17:09 acdlite

It seems onEnter will be called in createRoutes(routes) and before redux store ready.

// routerReplacement.js#L58
store = compose(
  applyMiddleware(
      replaceRoutesMiddleware(replaceRoutes)
  ),
  next({
      ...options,
      routes: createRoutes(routes)
  })
)(createStore)(reducer, initialState);

It's a hard problem with circular dependencies between redux store and react-router routes, @acdlite @gaearon Any ideas here?

chentsulin avatar Sep 25 '15 07:09 chentsulin

Would provide something like this solve the problem?

function createRoutes({ getState, isReady, addReadyListener }) {
  function requireAuth(nextState, replaceState, callback) {
    if (isReady()) {
       // ...
       callback();
    } else {
       addReadyListener(function() {
          // ...
          callback();
       });
    }
  }
  //...
}

But It looks not ideal...

chentsulin avatar Sep 30 '15 03:09 chentsulin

I'm running into this as well because dispatch also can't be called.

ddgromit avatar Oct 01 '15 23:10 ddgromit

Found another workaround:

import { Route, Redirect } from 'react-router';
import * as containers from './containers';

const {
  App,
  LoginPage,
  DashboardPage,
  NotFoundPage
} = containers;

export function createRoutes({ getState }) {
  function shouldNotAuth(nextState, replaceState, cb) {
    setTimeout(() => {
      const state = getState();
      if (state.user.current) {
        replaceState(null, '/dashboard');
      }
      cb();
    }, 0);
  }

  function requireAuth(nextState, replaceState, cb) {
    setTimeout(() => {
      const state = getState();
      if (!state.user.current) {
        replaceState(null, '/login');
      }
      cb();
    }, 0);
  }
  return (
    <Route component={App}>
      <Redirect from="/" to="login" />
      <Route path="login" component={LoginPage} onEnter={shouldNotAuth} />
      <Route onEnter={requireAuth}>
        <Route path="dashboard" component={DashboardPage} />
      </Route>
      <Route path="*" component={NotFoundPage} />
    </Route>
  );
}

But some warnings is inevitable:

Warning: Failed propType: Required prop `location` was not specified in `RoutingContext`. Check the render method of `ReduxRouterContext`.
Warning: Failed propType: Required prop `routes` was not specified in `RoutingContext`. Check the render method of `ReduxRouterContext`.
Warning: Failed propType: Required prop `params` was not specified in `RoutingContext`. Check the render method of `ReduxRouterContext`.
Warning: Failed propType: Required prop `components` was not specified in `RoutingContext`. Check the render method of `ReduxRouterContext`.
Warning: Failed Context Types: Required child context `location` was not specified in `RoutingContext`. Check the render method of `ReduxRouterContext`.

chentsulin avatar Oct 05 '15 03:10 chentsulin

Was looking into this issue a bit this evening.

It is caused by the history.listen call in the clients historySynchronization function, which results in redux-router match being called from useRoutes (this is called by the history listen function).

Changing the order of the compose arguments in client.js allows the tests to pass:

export default compose(
  useDefaults,
  historySynchronization,
  routeReplacement
)(reduxReactRouter);

I think this might still be an issue though since the store is being accessed before the state is fully initialized (router is null in the state object).

Any thoughts @acdlite?

tappleby avatar Oct 08 '15 02:10 tappleby

Using setImmediate also works similarly to @chentsulin's suggestion.

anthonator avatar Oct 13 '15 20:10 anthonator

Thanks for the workaround, @chentsulin!

Has anyone figured out how to get rid of the warnings?

lynndylanhurley avatar Oct 14 '15 00:10 lynndylanhurley

This is a dirty workaround and I'm not sure why it works but doing the following removes the warnings... cc @chentsulin @lynndylanhurley :

function requireAuth() {
    setTimeout(() => {
        let [nextState, replaceState, cb] = arguments;

        const state = getState();
        if (!state.user.current) {
            replaceState(null, '/login');
        }
        cb();
    }, 0);
}

Does anyone have a clue on first steps to fix this issue ?

cerisier avatar Oct 18 '15 23:10 cerisier

@cerisier - how and where are you defining getState?

lynndylanhurley avatar Oct 19 '15 19:10 lynndylanhurley

@lynndylanhurley In the createRoutes function. Here is the complete working example:

export function createRoutes({ getState }) {

  function requireAuth() {
    setTimeout(() => {
      let [nextState, replaceState, cb] = arguments;

      const state = getState();
      if (!state.user.current) {
          replaceState(null, '/login');
      }
      cb();
    }, 0);
  }

  return (
    <Route component={App}>
      <Route onEnter={requireAuth}>
        <Route path="dashboard" component={DashboardPage} />
      </Route>
      <Route path="*" component={NotFoundPage} />
    </Route>
  );
}

cerisier avatar Oct 20 '15 12:10 cerisier

@cerisier don't know why this can works ha

chentsulin avatar Oct 22 '15 09:10 chentsulin

I've tried this, but I still get the warnings. I should add that I'm using server side rendering, and the warnings are only thrown by the client after a server-side redirect.

lynndylanhurley avatar Oct 22 '15 17:10 lynndylanhurley

Any progress on this issue? I'm running into the same thing and I believe the discussion in #66 is mostly based around this as well.

stevenleeg avatar Oct 28 '15 00:10 stevenleeg

It would be really nice if the store was just available to the onEnter function. For example:

function requireAuth(nextState, replaceState, store, cb) {
  let state = store.getState();
  //...
}

The fact that it's not seriously complicates the redux configuration.

lynndylanhurley avatar Oct 28 '15 03:10 lynndylanhurley

Another way is to wrap onEnter to make it safe, like @stevoland has done here — https://github.com/erikras/react-redux-universal-hot-example/blob/master/src/helpers/makeRouteHooksSafe.js

ferdinandsalis avatar Oct 28 '15 13:10 ferdinandsalis

I am expecting a same API just like @lynndylanhurley suggested. This is a very normal requirement for onEnter hook access the state tree.

CrisLi avatar Oct 29 '15 03:10 CrisLi

Thanks @ferdinandsalis - I'll try that ASAP.

lynndylanhurley avatar Oct 29 '15 16:10 lynndylanhurley

It would be nice to have access to dispatch as well as state.

dsteinbach avatar Nov 02 '15 17:11 dsteinbach

@dsteinbach - the access to the store value would give both dispatch and getState. For example:

function requireAuth(nextState, replaceState, store, cb) {
  // access current state
  let state = store.getState();

  // dispatch event
  store.dispatch(someAction());
  //...
}

Whoever is maintaining this - will you accept a PR for this feature?

lynndylanhurley avatar Nov 02 '15 20:11 lynndylanhurley

For anybody who uses the workaround by @chentsulin , does the restricted route render first before it gets redirected to log in page?

hsin421 avatar Nov 04 '15 22:11 hsin421

@hsin421 - it doesn't for me.

lynndylanhurley avatar Nov 04 '15 22:11 lynndylanhurley

I'm using a workaround based on a higher-order component that wraps any components/views requiring authentication. I'm pretty new to this so if anyone has any comments or feedback I'd appreciate it.

routes.js

import {requireAuthentication} from 'components/AuthComponent';

export default (
  <Route path='/' component={CoreLayout}>
    <Route path="login" component={LoginView} />
    <Route path="protected" component={requireAuthentication(ProtectedView)}/>
  </Route>
);

AuthComponent.js

import React from 'react';
import { connect } from 'react-redux';
import { pushState } from 'redux-router';

export function requireAuthentication(Component) {

    class AuthComponent extends React.Component {

        componentWillMount() {
            if (!this.props.isAuthenticated) {
                // redirect to login and add next param so we can redirect again after login
                this.props.dispatch(pushState(null, `/login?next=${this.props.location.pathname}`));
            }
        }

        render() {
            // render the component that requires auth (passed to this wrapper)
            return (
                <Component  {...this.props} />
            )
        }
    }

    const mapStateToProps =
        (state) => ({
            token: state.auth.token,
            isAuthenticated: state.auth.isAuthenticated
        });

    return connect(mapStateToProps)(AuthComponent);

}

AuthComponent wraps the protected view and prevents it from rendering if the auth props do not indicate a successful login. In the componentDidMount of my protected views I am fetching data using the token passed by AuthComponent.

Anyone see any issues with this approach? Seems to be working well for me though I admit I have not spent a lot of time with it.

joshgeller avatar Nov 04 '15 22:11 joshgeller

@joshgeller your higher component works like a charm. Really appreciate it !!

I'll spend some more time with it and report any issues/improvements.

hsin421 avatar Nov 05 '15 00:11 hsin421

You'll also have to do the check in componentWillReceiveProps so if the user logouts or an API responds with 401 you can respond client-side. This works great for me thanks @joshgeller !

dsteinbach avatar Nov 05 '15 00:11 dsteinbach

Solved!!!!!!!!!!!!!!! It seems a problem on the configuration of the examples...

You have to set the routes with getRoutes function when you render the ReduxRouter component instead of doing this when you compose the store...

So that when you create the store, the routes configuration will not be calculated. In the Root component creation, you finally create the routes configuration, passing the store as parameter of the getRoutes function.

Simple two phase configuration.

Root.jsx

import React, { Component, PropTypes } from 'react';
import { Provider } from 'react-redux';
import { ReduxRouter } from 'redux-router';
import getRoutes from '../routes';

export default class Root extends Component {
  render() {
    const { store } = this.props;
    return (
      <Provider store={store}>
        <ReduxRouter>
          {getRoutes(store)}
        </ReduxRouter>
      </Provider>
    );
  }
}

Root.propTypes = {
  store: PropTypes.object.isRequired,
};

createStore.js

import createHistory from 'history/lib/createBrowserHistory';
import { createStore, applyMiddleware, compose } from 'redux';
import { apiMiddleware } from 'redux-api-middleware';
import createLogger from 'redux-logger';
import { reduxReactRouter } from 'redux-router';
import thunk from 'redux-thunk';
import DevTools from '../containers/DevTools';
import rootReducer from '../reducers';

const finalCreateStore = compose(
  applyMiddleware(thunk),
  applyMiddleware(createLogger()),
  reduxReactRouter( { createHistory } ),
  applyMiddleware(apiMiddleware),
  DevTools.instrument()
)(createStore);

export default function configureStore(initialState) {
  const store = finalCreateStore(rootReducer, initialState);

  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      const nextRootReducer = require('../reducers');
      store.replaceReducer(nextRootReducer);
    });
  }

  return store;
}

LucaColonnello avatar Nov 11 '15 16:11 LucaColonnello

@joshgeller I really liked the approach of high-level component. Looks very clear and solid. Seems like @LucaColonnello also makes a lot of sense, will try that out.

alexbeletsky avatar Nov 12 '15 10:11 alexbeletsky

@joshgeller Pretty sweet approach. I really like how readable the routes declaration is, you can easily see which routes require authentication.

So this is a issue coming up pretty often: It would be really appreciated if somebody could write something up, describing the workarounds and adding a simple example.

Scarysize avatar Nov 12 '15 11:11 Scarysize

I put together a sample repo and quick explanation of the higher-order component approach here:

https://github.com/joshgeller/react-redux-jwt-auth-example

joshgeller avatar Nov 12 '15 20:11 joshgeller

@joshgeller It looks like an amazing boilerplate.. Thanks for sharing!

alexbeletsky avatar Nov 12 '15 20:11 alexbeletsky