From bc0c9eeb8ef2c766b0612e99f57ba3790315caf1 Mon Sep 17 00:00:00 2001 From: Jimmy Jia Date: Sun, 30 Oct 2016 22:12:02 -0400 Subject: [PATCH] Expose router methods via context --- src/BaseLink.js | 27 +++++++++------------------ src/PropTypes.js | 11 +++++++++++ src/createBaseRouter.js | 35 ++++++++++++++++++++++++++++++++++- src/createConnectedLink.js | 8 ++++++-- src/createConnectedRouter.js | 36 ++++++++++++++++++++++++++++++++++-- src/createFarceRouter.js | 9 ++++++++- src/index.js | 1 + 7 files changed, 103 insertions(+), 24 deletions(-) create mode 100644 src/PropTypes.js diff --git a/src/BaseLink.js b/src/BaseLink.js index 30526212..58bdc0cf 100644 --- a/src/BaseLink.js +++ b/src/BaseLink.js @@ -1,6 +1,8 @@ import React from 'react'; import elementType from 'react-prop-types/lib/elementType'; +import { routerShape } from './PropTypes'; + const propTypes = { Component: elementType.isRequired, to: React.PropTypes.oneOfType([ @@ -14,19 +16,10 @@ const propTypes = { exact: React.PropTypes.bool.isRequired, target: React.PropTypes.string, onClick: React.PropTypes.func, - push: React.PropTypes.func.isRequired, }; const contextTypes = { - store: React.PropTypes.shape({ - farce: React.PropTypes.shape({ - createHref: React.PropTypes.func.isRequired, - createLocation: React.PropTypes.func.isRequired, - }).isRequired, - found: React.PropTypes.shape({ - isActive: React.PropTypes.func.isRequired, - }).isRequired, - }).isRequired, + router: routerShape.isRequired, }; const defaultProps = { @@ -36,7 +29,7 @@ const defaultProps = { class BaseLink extends React.Component { onClick = (event) => { - const { onClick, target, push, to } = this.props; + const { onClick, target, to } = this.props; if (onClick) { onClick(event); @@ -62,7 +55,7 @@ class BaseLink extends React.Component { // FIXME: When clicking on a link to the same location in the browser, the // actual becomes a replace rather than a push. We may want the same // handling – perhaps implemented in the Farce protocol. - push(to); + this.context.router.push(to); }; render() { @@ -77,13 +70,11 @@ class BaseLink extends React.Component { ...props } = this.props; - const { farce, found } = this.context.store; - - delete props.push; // Used in onClick. + const { router } = this.context; if (activeClassName || activeStyle || activePropName) { - const toLocation = farce.createLocation(to); - const active = found.isActive(toLocation, match, { exact }); + const toLocation = router.createLocation(to); + const active = router.isActive(toLocation, match, { exact }); if (active) { if (activeClassName) { @@ -104,7 +95,7 @@ class BaseLink extends React.Component { return ( ); diff --git a/src/PropTypes.js b/src/PropTypes.js new file mode 100644 index 00000000..aa535c84 --- /dev/null +++ b/src/PropTypes.js @@ -0,0 +1,11 @@ +import React from 'react'; + +export const routerShape = React.PropTypes.shape({ + push: React.PropTypes.func.isRequired, + replace: React.PropTypes.func.isRequired, + go: React.PropTypes.func.isRequired, + + createHref: React.PropTypes.func.isRequired, + createLocation: React.PropTypes.func.isRequired, + isActive: React.PropTypes.func.isRequired, +}); diff --git a/src/createBaseRouter.js b/src/createBaseRouter.js index a1ff8469..2d021733 100644 --- a/src/createBaseRouter.js +++ b/src/createBaseRouter.js @@ -2,6 +2,7 @@ import React from 'react'; import getRoutes from './getRoutes'; import HttpError from './HttpError'; +import { routerShape } from './PropTypes'; import RedirectException from './RedirectException'; export default function createBaseRouter({ routeConfig, matcher }) { @@ -10,10 +11,19 @@ export default function createBaseRouter({ routeConfig, matcher }) { matchContext: React.PropTypes.any, // eslint-disable-line react/no-unused-prop-types resolveElements: React.PropTypes.func.isRequired, // eslint-disable-line react/no-unused-prop-types render: React.PropTypes.func.isRequired, - replace: React.PropTypes.func.isRequired, // eslint-disable-line react/no-unused-prop-types + push: React.PropTypes.func.isRequired, + replace: React.PropTypes.func.isRequired, + go: React.PropTypes.func.isRequired, + createHref: React.PropTypes.func.isRequired, + createLocation: React.PropTypes.func.isRequired, + isActive: React.PropTypes.func.isRequired, initialState: React.PropTypes.object, }; + const childContextTypes = { + router: routerShape.isRequired, + }; + class BaseRouter extends React.Component { constructor(props, context) { super(props, context); @@ -31,6 +41,28 @@ export default function createBaseRouter({ routeConfig, matcher }) { } } + getChildContext() { + const { + push, + replace, + go, + createHref, + createLocation, + isActive, + } = this.props; + + return { + router: { + push, + replace, + go, + createHref, + createLocation, + isActive, + }, + }; + } + componentWillReceiveProps(nextProps) { ++this.matchIndex; this.resolveMatch(nextProps); @@ -94,6 +126,7 @@ export default function createBaseRouter({ routeConfig, matcher }) { } BaseRouter.propTypes = propTypes; + BaseRouter.childContextTypes = childContextTypes; return BaseRouter; } diff --git a/src/createConnectedLink.js b/src/createConnectedLink.js index a7ef8c6a..7214baa5 100644 --- a/src/createConnectedLink.js +++ b/src/createConnectedLink.js @@ -1,4 +1,3 @@ -import FarceActions from 'farce/lib/Actions'; import { connect } from 'react-redux'; import BaseLink from './BaseLink'; @@ -8,6 +7,11 @@ export default function createConnectedLink({ }) { return connect( state => ({ match: getMatch(state) }), - { push: FarceActions.push }, + null, + (stateProps, dispatchProps, ownProps) => ({ + ...ownProps, + ...stateProps, + // We don't want dispatch here. + }), )(BaseLink); } diff --git a/src/createConnectedRouter.js b/src/createConnectedRouter.js index ad0ca6e3..b2a9920f 100644 --- a/src/createConnectedRouter.js +++ b/src/createConnectedRouter.js @@ -1,4 +1,5 @@ import FarceActions from 'farce/lib/Actions'; +import React from 'react'; import { connect } from 'react-redux'; import createBaseRouter from './createBaseRouter'; @@ -7,8 +8,39 @@ export default function createConnectedRouter({ getMatch = ({ match }) => match, ...options }) { - return connect( + // createHref, createLocation, and isActive are taken directly from the store + // to avoid potential issues with middlewares in the chain messing with the + // return value from dispatch. + const propTypes = { + store: React.PropTypes.shape({ + farce: React.PropTypes.shape({ + createHref: React.PropTypes.func.isRequired, + createLocation: React.PropTypes.func.isRequired, + }).isRequired, + found: React.PropTypes.shape({ + isActive: React.PropTypes.func.isRequired, + }).isRequired, + }).isRequired, + }; + + const ConnectedRouter = connect( state => ({ match: getMatch(state) }), - { replace: FarceActions.replace }, + { + push: FarceActions.push, + replace: FarceActions.replace, + go: FarceActions.go, + }, + (stateProps, dispatchProps, { store, ...ownProps }) => ({ + ...ownProps, + ...stateProps, + ...dispatchProps, + createHref: store.farce.createHref, + createLocation: store.farce.createLocation, + isActive: store.found.isActive, + }), )(createBaseRouter(options)); + + ConnectedRouter.propTypes = propTypes; + + return ConnectedRouter; } diff --git a/src/createFarceRouter.js b/src/createFarceRouter.js index 85ba0ec2..01bc02ea 100644 --- a/src/createFarceRouter.js +++ b/src/createFarceRouter.js @@ -35,10 +35,17 @@ export default function createFarceRouter({ this.store.dispatch(FarceActions.init()); } + componentWillUnmount() { + this.store.dispatch(FarceActions.dispose()); + } + render() { return ( - + ); } diff --git a/src/index.js b/src/index.js index 77fdaf09..791fed23 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ export HttpError from './HttpError'; export Link from './Link'; export Matcher from './Matcher'; export matchReducer from './matchReducer'; +export { routerShape } from './PropTypes'; export Redirect from './Redirect'; export RedirectException from './RedirectException'; export resolveElements from './resolveElements';