From b7a3e02716eb9db875fd749cf682e8cbd67ef73e Mon Sep 17 00:00:00 2001 From: Matt Vague Date: Thu, 22 Nov 2018 16:37:51 -0500 Subject: [PATCH] Throw helpful error when router reducer not mounted under "router" (#175) Throw helpful error when router reducer not mounted under "router" --- src/ConnectedRouter.js | 10 ++++++---- src/selectors.js | 17 ++++++++++++++--- test/selectors.test.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/ConnectedRouter.js b/src/ConnectedRouter.js index 05c016e9..523f661a 100644 --- a/src/ConnectedRouter.js +++ b/src/ConnectedRouter.js @@ -3,9 +3,11 @@ import PropTypes from 'prop-types' import { connect } from 'react-redux' import { Router } from 'react-router' import { onLocationChanged } from './actions' +import createSelectors from './selectors' const createConnectedRouter = (structure) => { - const { getIn, toJS } = structure + const { getIn } = structure + const { getRouter, getLocation } = createSelectors(structure) /* * ConnectedRouter listens to a history object passed from props. * When history is changed, it dispatches action to redux store. @@ -26,7 +28,7 @@ const createConnectedRouter = (structure) => { pathname: pathnameInStore, search: searchInStore, hash: hashInStore, - } = toJS(getIn(context.store.getState(), ['router', 'location'])) + } = getLocation(context.store.getState()) // Extract history's location const { pathname: pathnameInHistory, @@ -102,8 +104,8 @@ const createConnectedRouter = (structure) => { } const mapStateToProps = state => ({ - action: getIn(state, ['router', 'action']), - location: getIn(state, ['router', 'location']), + action: getIn(getRouter(state), ['action']), + location: getIn(getRouter(state), ['location']), }) const mapDispatchToProps = dispatch => ({ diff --git a/src/selectors.js b/src/selectors.js index 636c9ef0..e322b466 100644 --- a/src/selectors.js +++ b/src/selectors.js @@ -2,8 +2,19 @@ import { matchPath } from "react-router" const createSelectors = (structure) => { const { getIn, toJS } = structure - const getLocation = state => toJS(getIn(state, ['router', 'location'])) - const getAction = state => toJS(getIn(state, ['router', 'action'])) + + const isRouter = (value) => value != null && + typeof value === 'object' && + getIn(value, ['location']) && + getIn(value, ['action']) + + const getRouter = state => { + const router = toJS(getIn(state, ['router'])) + if (!isRouter(router)) { throw 'Could not find router reducer in state tree, it must be mounted under "router"' } + return router + } + const getLocation = state => toJS(getIn(getRouter(state), ['location'])) + const getAction = state => toJS(getIn(getRouter(state), ['action'])) // It only makes sense to recalculate the `matchPath` whenever the pathname // of the location changes. That's why `createMatchSelector` memoizes @@ -27,7 +38,7 @@ const createSelectors = (structure) => { } } - return {getLocation, getAction, createMatchSelector} + return {getLocation, getAction, getRouter, createMatchSelector} } export default createSelectors diff --git a/test/selectors.test.js b/test/selectors.test.js index ef40ef02..59c8784c 100644 --- a/test/selectors.test.js +++ b/test/selectors.test.js @@ -23,6 +23,36 @@ describe("selectors", () => { store = createStore(reducer) }) + describe("when router not found under 'router' key", () => { + beforeEach(() => { + const reducer = combineReducers({ + notTheRouter: connectRouter(history) + }) + store = createStore(reducer) + }) + + it("throws helpful error", () => { + store.dispatch(push('/')) + const state = store.getState() + expect(() => getLocation(state)).toThrowError(/^Could not find router reducer in state tree, it must be mounted under "router"$/) + }) + }) + + describe("when something else found under 'router' key", () => { + beforeEach(() => { + const reducer = combineReducers({ + router: () => ({ some: 'thing' }) + }) + store = createStore(reducer) + }) + + it("throws helpful error", () => { + store.dispatch(push('/')) + const state = store.getState() + expect(() => getLocation(state)).toThrowError(/^Could not find router reducer in state tree, it must be mounted under "router"$/) + }) + }) + describe("getLocation", () => { it("gets the location from the state", () => { const location = { pathname: "/", hash: '', search: '' }