UNPKG

react-router

Version:

A complete routing library for React.js

309 lines (253 loc) • 8.85 kB
import React, { createElement, isValidElement } from 'react'; import warning from 'warning'; import invariant from 'invariant'; import { loopAsync } from './AsyncUtils'; import { createRoutes } from './RouteUtils'; import { getState, getTransitionHooks, getComponents, getRouteParams, createTransitionHook } from './RoutingUtils'; import { routes, component, components, history, location } from './PropTypes'; import RouterContextMixin from './RouterContextMixin'; import ScrollManagementMixin from './ScrollManagementMixin'; import { isLocation } from './Location'; import Transition from './Transition'; var { arrayOf, func, object } = React.PropTypes; function runTransition(prevState, routes, location, hooks, callback) { var transition = new Transition; getState(routes, location, function (error, nextState) { if (error || nextState == null || transition.isCancelled) { callback(error, null, transition); } else { nextState.location = location; var transitionHooks = getTransitionHooks(prevState, nextState); if (Array.isArray(hooks)) transitionHooks.unshift.apply(transitionHooks, hooks); loopAsync(transitionHooks.length, (index, next, done) => { transitionHooks[index](nextState, transition, (error) => { if (error || transition.isCancelled) { done(error); // No need to continue. } else { next(); } }); }, function (error) { if (error || transition.isCancelled) { callback(error, null, transition); } else { getComponents(nextState.branch, function (error, components) { if (error || transition.isCancelled) { callback(error, null, transition); } else { nextState.components = components; callback(null, nextState, transition); } }); } }); } }); } var Router = React.createClass({ mixins: [ RouterContextMixin, ScrollManagementMixin ], statics: { /** * Runs a transition to the given location using the given routes and * transition hooks (optional) and calls callback(error, state, transition) * when finished. This is primarily useful for server-side rendering. */ run(routes, location, transitionHooks, callback) { if (typeof transitionHooks === 'function') { callback = transitionHooks; transitionHooks = null; } invariant( typeof callback === 'function', 'Router.run needs a callback' ); runTransition(null, routes, location, transitionHooks, callback); } }, propTypes: { createElement: func.isRequired, onAbort: func, onError: func, onUpdate: func, // Client-side history, routes, // Routes may also be given as children (JSX) children: routes, // Server-side location, branch: routes, params: object, components: arrayOf(components) }, getDefaultProps() { return { createElement }; }, getInitialState() { return { isTransitioning: false, location: null, branch: null, params: null, components: null }; }, _updateState(location) { invariant( isLocation(location), 'A <Router> needs a valid Location' ); var hooks = this.transitionHooks; if (hooks) hooks = hooks.map(hook => createTransitionHook(hook, this)); this.setState({ isTransitioning: true }); runTransition(this.state, this.routes, location, hooks, (error, state, transition) => { if (error) { this.handleError(error); } else if (transition.isCancelled) { if (transition.redirectInfo) { var { pathname, query, state } = transition.redirectInfo; this.replaceWith(pathname, query, state); } else { invariant( this.state.location, 'You may not abort the initial transition' ); this.handleAbort(reason); } } else if (state == null) { warning(false, 'Location "%s" did not match any routes', location.pathname); } else { this.setState(state, this.props.onUpdate); } this.setState({ isTransitioning: false }); }); }, /** * Adds a transition hook that runs before all route hooks in a * transition. The signature is the same as route transition hooks. */ addTransitionHook(hook) { if (!this.transitionHooks) this.transitionHooks = []; this.transitionHooks.push(hook); }, /** * Removes the given transition hook. */ removeTransitionHook(hook) { if (this.transitionHooks) this.transitionHooks = this.transitionHooks.filter(h => h !== hook); }, handleAbort(reason) { if (this.props.onAbort) { this.props.onAbort.call(this, reason); } else { // The best we can do here is goBack so the location state reverts // to what it was. However, we also set a flag so that we know not // to run through _updateState again since state did not change. this._ignoreNextHistoryChange = true; this.goBack(); } }, handleError(error) { if (this.props.onError) { this.props.onError.call(this, error); } else { // Throw errors by default so we don't silently swallow them! throw error; // This error probably originated in getChildRoutes or getComponents. } }, handleHistoryChange() { if (this._ignoreNextHistoryChange) { this._ignoreNextHistoryChange = false; } else { this._updateState(this.props.history.location); } }, componentWillMount() { var { history, routes, children, location, branch, params, components } = this.props; if (history) { invariant( routes || children, 'Client-side <Router>s need routes. Try using <Router routes> or ' + 'passing your routes as nested <Route> children' ); this.routes = createRoutes(routes || children); if (typeof history.setup === 'function') history.setup(); // We need to listen first in case we redirect immediately. if (history.addChangeListener) history.addChangeListener(this.handleHistoryChange); this._updateState(history.location); } else { invariant( location && branch && params && components, 'Server-side <Router>s need location, branch, params, and components ' + 'props. Try using Router.run to get all the props you need' ); this.setState({ location, branch, params, components }); } }, componentWillReceiveProps(nextProps) { invariant( this.props.history === nextProps.history, '<Router history> may not be changed' ); if (nextProps.history) { var currentRoutes = this.props.routes || this.props.children; var nextRoutes = nextProps.routes || nextProps.children; if (currentRoutes !== nextRoutes) { this.routes = createRoutes(nextRoutes); // Call this here because _updateState // uses this.routes to determine state. if (nextProps.history.location) this._updateState(nextProps.history.location); } } }, componentWillUnmount() { var { history } = this.props; if (history && history.removeChangeListener) history.removeChangeListener(this.handleHistoryChange); }, _createElement(component, props) { return typeof component === 'function' ? this.props.createElement(component, props) : null; }, render() { var { location, branch, params, components, isTransitioning } = this.state; var element = null; if (components) { element = components.reduceRight((element, components, index) => { if (components == null) return element; // Don't create new children; use the grandchildren. var route = branch[index]; var routeParams = getRouteParams(route, params); var props = { location, params, route, routeParams, isTransitioning }; if (isValidElement(element)) { props.children = element; } else if (element) { // In render, do var { header, sidebar } = this.props; Object.assign(props, element); } if (typeof components === 'object') { var elements = {}; for (var key in components) if (components.hasOwnProperty(key)) elements[key] = this._createElement(components[key], props); return elements; } return this._createElement(components, props); }, element); } invariant( element === null || element === false || isValidElement(element), 'The root route must render a single element' ); return element; } }); export default Router;