├── tests.node.js ├── logo ├── horizontal.png ├── vertical.png ├── vertical@2x.png └── horizontal@2x.png ├── modules ├── __tests__ │ ├── .eslintrc │ ├── shouldWarn.js │ ├── resetHash.js │ ├── execSteps.js │ ├── getParamNames-test.js │ ├── IndexRedirect-test.js │ ├── withRouter-test.js │ ├── _bc-History-test.js │ ├── RouteComponent-test.js │ ├── matchPattern-test.js │ ├── AsyncUtils-test.js │ ├── push-test.js │ ├── Redirect-test.js │ ├── useRouterHistory-test.js │ ├── createRoutesFromReactChildren-test.js │ ├── IndexRoute-test.js │ └── _bc-serverRendering-test.js ├── hashHistory.js ├── browserHistory.js ├── IndexLink.js ├── createRouterHistory.js ├── useRouterHistory.js ├── RoutingContext.js ├── routerWarning.js ├── History.js ├── getRouteParams.js ├── createMemoryHistory.js ├── withRouter.js ├── RouterUtils.js ├── InternalPropTypes.js ├── RouteContext.js ├── applyRouterMiddleware.js ├── useRoutes.js ├── index.js ├── Route.js ├── IndexRedirect.js ├── IndexRoute.js ├── deprecateObjectProperties.js ├── AsyncUtils.js ├── getComponents.js ├── match.js ├── Lifecycle.js ├── computeChangedRoutes.js ├── Redirect.js ├── PropTypes.js ├── RouteUtils.js ├── TransitionUtils.js ├── RouterContext.js └── Link.js ├── .gitignore ├── examples ├── auth-with-shared-root │ ├── components │ │ ├── About.js │ │ ├── PageOne.js │ │ ├── PageTwo.js │ │ ├── User.js │ │ ├── Logout.js │ │ ├── Dashboard.js │ │ ├── Landing.js │ │ ├── App.js │ │ └── Login.js │ ├── app.js │ ├── index.html │ ├── utils │ │ └── auth.js │ └── config │ │ └── routes.js ├── huge-apps │ ├── routes │ │ ├── Grades │ │ │ ├── index.js │ │ │ └── components │ │ │ │ └── Grades.js │ │ ├── Profile │ │ │ ├── index.js │ │ │ └── components │ │ │ │ └── Profile.js │ │ ├── Calendar │ │ │ ├── index.js │ │ │ └── components │ │ │ │ └── Calendar.js │ │ ├── Messages │ │ │ ├── index.js │ │ │ └── components │ │ │ │ └── Messages.js │ │ └── Course │ │ │ ├── routes │ │ │ ├── Grades │ │ │ │ ├── index.js │ │ │ │ └── components │ │ │ │ │ └── Grades.js │ │ │ ├── Assignments │ │ │ │ ├── routes │ │ │ │ │ └── Assignment │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── components │ │ │ │ │ │ └── Assignment.js │ │ │ │ ├── components │ │ │ │ │ ├── Assignments.js │ │ │ │ │ └── Sidebar.js │ │ │ │ └── index.js │ │ │ └── Announcements │ │ │ │ ├── routes │ │ │ │ └── Announcement │ │ │ │ │ ├── index.js │ │ │ │ │ └── components │ │ │ │ │ └── Announcement.js │ │ │ │ ├── components │ │ │ │ ├── Announcements.js │ │ │ │ └── Sidebar.js │ │ │ │ └── index.js │ │ │ ├── components │ │ │ ├── Dashboard.js │ │ │ ├── Nav.js │ │ │ └── Course.js │ │ │ └── index.js │ ├── components │ │ ├── App.js │ │ ├── Dashboard.js │ │ └── GlobalNav.js │ ├── index.html │ ├── stubs │ │ └── COURSES.js │ └── app.js ├── README.md ├── global.css ├── sidebar │ ├── index.html │ ├── app.css │ ├── data.js │ └── app.js ├── pinterest │ ├── index.html │ └── app.js ├── active-links │ ├── index.html │ └── app.js ├── query-params │ ├── index.html │ └── app.js ├── master-detail │ ├── index.html │ ├── app.css │ └── ContactStore.js ├── route-no-match │ ├── index.html │ └── app.js ├── auth-flow │ ├── index.html │ ├── auth.js │ └── app.js ├── dynamic-segments │ ├── index.html │ └── app.js ├── nested-animations │ ├── index.html │ ├── app.css │ └── app.js ├── animations │ ├── index.html │ ├── app.css │ └── app.js ├── breadcrumbs │ ├── index.html │ ├── app.css │ └── app.js ├── confirming-navigation │ ├── index.html │ └── app.js ├── passing-props-to-children │ ├── index.html │ ├── app.css │ └── app.js ├── auth-flow-async-with-query-params │ ├── index.html │ └── app.js ├── server.js ├── webpack.config.js └── index.html ├── .eslintrc ├── .babelrc ├── SPONSORS.md ├── .travis.yml ├── scripts ├── build.js └── release.sh ├── docs ├── guides │ ├── README.md │ ├── NavigatingOutsideOfComponents.md │ ├── ConfirmingNavigation.md │ ├── MinimizingBundleSize.md │ ├── RouteMatching.md │ ├── IndexRoutes.md │ ├── DynamicRouting.md │ ├── ServerRendering.md │ ├── ComponentLifecycle.md │ └── testing.md ├── README.md └── Troubleshooting.md ├── webpack.config.js ├── upgrade-guides ├── v2.2.0.md └── v2.4.0.md ├── tests.webpack.js ├── LICENSE.md ├── ISSUE_TEMPLATE.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── package.json └── karma.conf.js /tests.node.js: -------------------------------------------------------------------------------- 1 | import './modules/__tests__/serverRendering-test' 2 | -------------------------------------------------------------------------------- /logo/horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/react-router/master/logo/horizontal.png -------------------------------------------------------------------------------- /logo/vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/react-router/master/logo/vertical.png -------------------------------------------------------------------------------- /logo/vertical@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/react-router/master/logo/vertical@2x.png -------------------------------------------------------------------------------- /logo/horizontal@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/react-router/master/logo/horizontal@2x.png -------------------------------------------------------------------------------- /modules/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "react/prop-types": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | es6 2 | lib 3 | umd 4 | examples/**/*-bundle.js 5 | node_modules 6 | npm-debug.log 7 | website/index.html 8 | website/tags/* 9 | coverage 10 | -------------------------------------------------------------------------------- /modules/__tests__/shouldWarn.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0*/ 2 | 3 | export default function shouldWarn(about) { 4 | console.error.expected.push(about) 5 | } 6 | -------------------------------------------------------------------------------- /modules/hashHistory.js: -------------------------------------------------------------------------------- 1 | import createHashHistory from 'history/lib/createHashHistory' 2 | import createRouterHistory from './createRouterHistory' 3 | export default createRouterHistory(createHashHistory) 4 | 5 | -------------------------------------------------------------------------------- /modules/browserHistory.js: -------------------------------------------------------------------------------- 1 | import createBrowserHistory from 'history/lib/createBrowserHistory' 2 | import createRouterHistory from './createRouterHistory' 3 | export default createRouterHistory(createBrowserHistory) 4 | 5 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/components/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const About = React.createClass({ 4 | render() { 5 | return

About

6 | } 7 | }) 8 | 9 | export default About 10 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/components/PageOne.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const PageOne = React.createClass({ 4 | render() { 5 | return

Page One!

6 | } 7 | }) 8 | 9 | export default PageOne 10 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/components/PageTwo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const PageTwo = React.createClass({ 4 | render() { 5 | return

Page Two! Wooo!

6 | } 7 | }) 8 | 9 | export default PageTwo 10 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Grades/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'grades', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('./components/Grades')) 6 | }) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Profile/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'profile', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('./components/Profile')) 6 | }) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/components/User.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const User = React.createClass({ 4 | render() { 5 | return

User: {this.props.params.id}

6 | } 7 | }) 8 | 9 | export default User 10 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Calendar/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'calendar', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('./components/Calendar')) 6 | }) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Messages/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'messages', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('./components/Messages')) 6 | }) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Grades/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'grades', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('./components/Grades')) 6 | }) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/__tests__/resetHash.js: -------------------------------------------------------------------------------- 1 | function resetHash(done) { 2 | if (window.location.hash !== '') { 3 | window.location.hash = '' 4 | setTimeout(done, 10) 5 | } else { 6 | done() 7 | } 8 | } 9 | 10 | export default resetHash 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "rackt", 3 | "globals": { 4 | "__DEV__": true 5 | }, 6 | "rules": { 7 | "react/jsx-uses-react": 1, 8 | "react/jsx-no-undef": 2, 9 | "react/wrap-multilines": 2 10 | }, 11 | "plugins": [ 12 | "react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Assignments/routes/Assignment/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: ':assignmentId', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('./components/Assignment')) 6 | }) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | React Router Examples 2 | ===================== 3 | 4 | To run the examples in your development environment: 5 | 6 | 1. Clone this repo 7 | 2. Run `npm install` 8 | 3. Start the development server with `npm start` 9 | 4. Point your browser to http://localhost:8080 10 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router } from 'react-router' 4 | import routes from './config/routes' 5 | 6 | render(, document.getElementById('example')) 7 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Announcements/routes/Announcement/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: ':announcementId', 3 | 4 | getComponent(nextState, cb) { 5 | require.ensure([], (require) => { 6 | cb(null, require('./components/Announcement')) 7 | }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Grades/components/Grades.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class Grades extends React.Component { 4 | 5 | render() { 6 | return ( 7 |
8 |

Grades

9 |
10 | ) 11 | } 12 | 13 | } 14 | 15 | module.exports = Grades 16 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Profile/components/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class Profile extends Component { 4 | render() { 5 | return ( 6 |
7 |

Profile

8 |
9 | ) 10 | } 11 | } 12 | 13 | module.exports = Profile 14 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class Dashboard extends Component { 4 | render() { 5 | return ( 6 |
7 |

Course Dashboard

8 |
9 | ) 10 | } 11 | } 12 | 13 | export default Dashboard 14 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Messages/components/Messages.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class Messages extends Component { 4 | 5 | render() { 6 | return ( 7 |
8 |

Messages

9 |
10 | ) 11 | } 12 | 13 | } 14 | 15 | module.exports = Messages 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react"], 3 | "plugins": ["dev-expression"], 4 | 5 | "env": { 6 | "cjs": { 7 | "presets": ["es2015-loose", "stage-1"], 8 | "plugins": ["add-module-exports"] 9 | }, 10 | "es": { 11 | "presets": ["es2015-loose-native-modules", "stage-1"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Arial; 3 | font-weight: 200; 4 | } 5 | 6 | h1, h2, h3 { 7 | font-weight: 100; 8 | } 9 | 10 | a { 11 | color: hsl(200, 50%, 50%); 12 | } 13 | 14 | a.active { 15 | color: hsl(20, 50%, 50%); 16 | } 17 | 18 | .breadcrumbs a { 19 | text-decoration: none; 20 | } 21 | -------------------------------------------------------------------------------- /SPONSORS.md: -------------------------------------------------------------------------------- 1 | The following companies have provided sponsorship toward the development 2 | of React Router. Thank you! 3 | 4 | - [React Training](https://reactjs-training.com) 5 | - [![Modus Create](http://i.imgur.com/FxzUtvl.png)](http://moduscreate.com/) 6 | - [![Instructure](http://i.imgur.com/kMZauLm.png)](https://www.instructure.com/) 7 | 8 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/components/Logout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import auth from '../utils/auth' 3 | 4 | const Logout = React.createClass({ 5 | componentDidMount() { 6 | auth.logout() 7 | }, 8 | 9 | render() { 10 | return

You are now logged out

11 | } 12 | }) 13 | 14 | export default Logout 15 | -------------------------------------------------------------------------------- /modules/IndexLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from './Link' 3 | 4 | /** 5 | * An is used to link to an . 6 | */ 7 | const IndexLink = React.createClass({ 8 | 9 | render() { 10 | return 11 | } 12 | 13 | }) 14 | 15 | export default IndexLink 16 | -------------------------------------------------------------------------------- /modules/createRouterHistory.js: -------------------------------------------------------------------------------- 1 | import useRouterHistory from './useRouterHistory' 2 | 3 | const canUseDOM = !!( 4 | typeof window !== 'undefined' && window.document && window.document.createElement 5 | ) 6 | 7 | export default function (createHistory) { 8 | let history 9 | if (canUseDOM) 10 | history = useRouterHistory(createHistory)() 11 | return history 12 | } 13 | -------------------------------------------------------------------------------- /modules/useRouterHistory.js: -------------------------------------------------------------------------------- 1 | import useQueries from 'history/lib/useQueries' 2 | import useBasename from 'history/lib/useBasename' 3 | 4 | export default function useRouterHistory(createHistory) { 5 | return function (options) { 6 | const history = useQueries(useBasename(createHistory))(options) 7 | history.__v2_compatible__ = true 8 | return history 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/sidebar/index.html: -------------------------------------------------------------------------------- 1 | 2 | Sidebar Example 3 | 4 | 5 | 6 |

React Router Examples / Sidebar

7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/pinterest/index.html: -------------------------------------------------------------------------------- 1 | 2 | Pinterest-style UI Example 3 | 4 | 5 | 6 |

React Router Examples / Pinterest

7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/active-links/index.html: -------------------------------------------------------------------------------- 1 | 2 | Active Links Example 3 | 4 | 5 | 6 |

React Router Examples / Active Links

7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Assignments/components/Assignments.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class Assignments extends Component { 4 | render() { 5 | return ( 6 |
7 |

Assignments

8 | {this.props.children ||

Choose an assignment from the sidebar.

} 9 |
10 | ) 11 | } 12 | } 13 | 14 | module.exports = Assignments 15 | -------------------------------------------------------------------------------- /examples/query-params/index.html: -------------------------------------------------------------------------------- 1 | 2 | Query Params Example 3 | 4 | 5 | 6 |

React Router Examples / Query Params

7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/master-detail/index.html: -------------------------------------------------------------------------------- 1 | 2 | Master Detail Example 3 | 4 | 5 | 6 |

React Router Examples / Master Detail

7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/route-no-match/index.html: -------------------------------------------------------------------------------- 1 | 2 | Route Not Found Example 3 | 4 | 5 | 6 |

React Router Examples / Route Not Found

7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/auth-flow/index.html: -------------------------------------------------------------------------------- 1 | 2 | Authentication Flow Example 3 | 4 | 5 | 6 |

React Router Examples / Auth Flow

7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Announcements/components/Announcements.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class Announcements extends Component { 4 | render() { 5 | return ( 6 |
7 |

Announcements

8 | {this.props.children ||

Choose an announcement from the sidebar.

} 9 |
10 | ) 11 | } 12 | } 13 | 14 | module.exports = Announcements 15 | -------------------------------------------------------------------------------- /examples/dynamic-segments/index.html: -------------------------------------------------------------------------------- 1 | 2 | Dynamic Segments Example 3 | 4 | 5 | 6 |

React Router Examples / Dynamic Segments

7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/nested-animations/index.html: -------------------------------------------------------------------------------- 1 | 2 | Nested Animations Example 3 | 4 | 5 | 6 |

React Router Examples / Nested Animations

7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /modules/__tests__/execSteps.js: -------------------------------------------------------------------------------- 1 | function execSteps(steps, done) { 2 | let index = 0 3 | 4 | return function () { 5 | if (steps.length === 0) { 6 | done() 7 | } else { 8 | try { 9 | steps[index++].apply(this, arguments) 10 | 11 | if (index === steps.length) 12 | done() 13 | } catch (error) { 14 | done(error) 15 | } 16 | } 17 | } 18 | } 19 | 20 | export default execSteps 21 | -------------------------------------------------------------------------------- /examples/animations/index.html: -------------------------------------------------------------------------------- 1 | 2 | Animation Example 3 | 4 | 5 | 6 | 7 |

React Router Examples / Animations

8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/breadcrumbs/index.html: -------------------------------------------------------------------------------- 1 | 2 | Breadcrumbs Example 3 | 4 | 5 | 6 | 7 |

React Router Examples / Breadcrumbs

8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/confirming-navigation/index.html: -------------------------------------------------------------------------------- 1 | 2 | Confirming Navigation Example 3 | 4 | 5 | 6 |

React Router Examples / Confirming Navigation

7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import auth from '../utils/auth' 3 | 4 | const Dashboard = React.createClass({ 5 | render() { 6 | const token = auth.getToken() 7 | 8 | return ( 9 |
10 |

Dashboard

11 |

You made it!

12 |

{token}

13 | {this.props.children} 14 |
15 | ) 16 | } 17 | }) 18 | 19 | export default Dashboard 20 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/index.html: -------------------------------------------------------------------------------- 1 | 2 | Authentication With Shared Root Example 3 | 4 | 5 | 6 |

React Router Examples / Auth With Shared Root

7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - stable 5 | cache: 6 | directories: 7 | - node_modules 8 | before_install: 9 | - export CHROME_BIN=chromium-browser 10 | - export DISPLAY=:99.0 11 | - sh -e /etc/init.d/xvfb start 12 | after_success: 13 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 14 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 15 | branches: 16 | only: 17 | - master 18 | - next 19 | -------------------------------------------------------------------------------- /examples/passing-props-to-children/index.html: -------------------------------------------------------------------------------- 1 | 2 | Passing Props to Children Example 3 | 4 | 5 | 6 |

React Router Examples / Passing Props to Children

7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/sidebar/app.css: -------------------------------------------------------------------------------- 1 | .Sidebar { 2 | float: left; 3 | background: #eee; 4 | padding: 20px; 5 | margin: 0 20px 20px 20px; 6 | width: 200px; 7 | cursor: pointer; 8 | } 9 | 10 | .Content { 11 | padding: 20px 20px 20px 300px; 12 | } 13 | 14 | .CategoryNav__Toggle:before { 15 | display: inline-block; 16 | width: 1em; 17 | content: '▸'; 18 | } 19 | 20 | .CategoryNav__Toggle--is-open:before { 21 | content: '▾'; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | } 27 | -------------------------------------------------------------------------------- /examples/huge-apps/components/App.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React, { Component } from 'react' 3 | import Dashboard from './Dashboard' 4 | import GlobalNav from './GlobalNav' 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 | {this.props.children || } 13 |
14 |
15 | ) 16 | } 17 | } 18 | 19 | module.exports = App 20 | -------------------------------------------------------------------------------- /examples/animations/app.css: -------------------------------------------------------------------------------- 1 | .Image { 2 | position: absolute; 3 | height: 400px; 4 | width: 400px; 5 | } 6 | 7 | .example-enter { 8 | opacity: 0.01; 9 | transition: opacity .5s ease-in; 10 | } 11 | 12 | .example-enter.example-enter-active { 13 | opacity: 1; 14 | } 15 | 16 | .example-leave { 17 | opacity: 1; 18 | transition: opacity .5s ease-in; 19 | } 20 | 21 | .example-leave.example-leave-active { 22 | opacity: 0; 23 | } 24 | 25 | .link-active { 26 | color: #bbbbbb; 27 | text-decoration: none; 28 | } 29 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/components/Landing.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Landing = React.createClass({ 4 | 5 | render() { 6 | return ( 7 |
8 |

Landing Page

9 |

This page is only shown to unauthenticated users.

10 |

Partial / Lazy loading. Open the network tab while you navigate. Notice that only the required components are downloaded as you navigate around.

11 |
12 | ) 13 | } 14 | 15 | }) 16 | 17 | export default Landing 18 | -------------------------------------------------------------------------------- /examples/huge-apps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Huge Apps Example 5 | 6 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'course/:courseId', 3 | 4 | getChildRoutes(location, cb) { 5 | require.ensure([], (require) => { 6 | cb(null, [ 7 | require('./routes/Announcements'), 8 | require('./routes/Assignments'), 9 | require('./routes/Grades') 10 | ]) 11 | }) 12 | }, 13 | 14 | getComponent(nextState, cb) { 15 | require.ensure([], (require) => { 16 | cb(null, require('./components/Course')) 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Calendar/components/Calendar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class Calendar extends Component { 4 | render() { 5 | const events = [ 6 | { id: 0, title: 'essay due' } 7 | ] 8 | 9 | return ( 10 |
11 |

Calendar

12 |
    13 | {events.map(event => ( 14 |
  • {event.title}
  • 15 | ))} 16 |
17 |
18 | ) 19 | } 20 | } 21 | 22 | module.exports = Calendar 23 | -------------------------------------------------------------------------------- /modules/RoutingContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import RouterContext from './RouterContext' 3 | import warning from './routerWarning' 4 | 5 | const RoutingContext = React.createClass({ 6 | componentWillMount() { 7 | warning(false, '`RoutingContext` has been renamed to `RouterContext`. Please use `import { RouterContext } from \'react-router\'`. http://tiny.cc/router-routercontext') 8 | }, 9 | 10 | render() { 11 | return 12 | } 13 | }) 14 | 15 | export default RoutingContext 16 | -------------------------------------------------------------------------------- /modules/routerWarning.js: -------------------------------------------------------------------------------- 1 | import warning from 'warning' 2 | 3 | let warned = {} 4 | 5 | export default function routerWarning(falseToWarn, message, ...args) { 6 | // Only issue deprecation warnings once. 7 | if (message.indexOf('deprecated') !== -1) { 8 | if (warned[message]) { 9 | return 10 | } 11 | 12 | warned[message] = true 13 | } 14 | 15 | message = `[react-router] ${message}` 16 | warning(falseToWarn, message, ...args) 17 | } 18 | 19 | export function _resetWarned() { 20 | warned = {} 21 | } 22 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | var execSync = require('child_process').execSync 2 | var readFileSync = require('fs').readFileSync 3 | var prettyBytes = require('pretty-bytes') 4 | var gzipSize = require('gzip-size') 5 | 6 | function exec(command) { 7 | execSync(command, { stdio: [0, 1, 2] }) 8 | } 9 | 10 | exec('npm run build') 11 | exec('npm run build-umd') 12 | exec('npm run build-min') 13 | 14 | console.log( 15 | '\ngzipped, the UMD build is ' + prettyBytes( 16 | gzipSize.sync(readFileSync('umd/ReactRouter.min.js')) 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Assignments/routes/Assignment/components/Assignment.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React, { Component } from 'react' 3 | 4 | class Assignment extends Component { 5 | render() { 6 | let { courseId, assignmentId } = this.props.params 7 | let { title, body } = COURSES[courseId].assignments[assignmentId] 8 | 9 | return ( 10 |
11 |

{title}

12 |

{body}

13 |
14 | ) 15 | } 16 | } 17 | 18 | module.exports = Assignment 19 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Assignments/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'assignments', 3 | 4 | getChildRoutes(location, cb) { 5 | require.ensure([], (require) => { 6 | cb(null, [ 7 | require('./routes/Assignment') 8 | ]) 9 | }) 10 | }, 11 | 12 | getComponents(nextState, cb) { 13 | require.ensure([], (require) => { 14 | cb(null, { 15 | sidebar: require('./components/Sidebar'), 16 | main: require('./components/Assignments') 17 | }) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Announcements/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'announcements', 3 | 4 | getChildRoutes(location, cb) { 5 | require.ensure([], (require) => { 6 | cb(null, [ 7 | require('./routes/Announcement') 8 | ]) 9 | }) 10 | }, 11 | 12 | getComponents(nextState, cb) { 13 | require.ensure([], (require) => { 14 | cb(null, { 15 | sidebar: require('./components/Sidebar'), 16 | main: require('./components/Announcements') 17 | }) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Announcements/routes/Announcement/components/Announcement.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React, { Component } from 'react' 3 | 4 | class Announcement extends Component { 5 | render() { 6 | let { courseId, announcementId } = this.props.params 7 | let { title, body } = COURSES[courseId].announcements[announcementId] 8 | 9 | return ( 10 |
11 |

{title}

12 |

{body}

13 |
14 | ) 15 | } 16 | } 17 | 18 | module.exports = Announcement 19 | -------------------------------------------------------------------------------- /docs/guides/README.md: -------------------------------------------------------------------------------- 1 | # Guides 2 | 3 | * [Route Configuration](RouteConfiguration.md) 4 | * [Route Matching](RouteMatching.md) 5 | * [Histories](Histories.md) 6 | * [Index Routes and Links](IndexRoutes.md) 7 | * [Testing](testing.md) 8 | * [Dynamic Routing](DynamicRouting.md) 9 | * [Confirming Navigation](ConfirmingNavigation.md) 10 | * [Server Rendering](ServerRendering.md) 11 | * [Component Lifecycle](ComponentLifecycle.md) 12 | * [Navigating Outside of Components](NavigatingOutsideOfComponents.md) 13 | * [Minimizing Bundle Size](MinimizingBundleSize.md) 14 | -------------------------------------------------------------------------------- /modules/History.js: -------------------------------------------------------------------------------- 1 | import warning from './routerWarning' 2 | import { history } from './InternalPropTypes' 3 | 4 | /** 5 | * A mixin that adds the "history" instance variable to components. 6 | */ 7 | const History = { 8 | 9 | contextTypes: { 10 | history 11 | }, 12 | 13 | componentWillMount() { 14 | warning(false, 'the `History` mixin is deprecated, please access `context.router` with your own `contextTypes`. http://tiny.cc/router-historymixin') 15 | this.history = this.context.history 16 | } 17 | 18 | } 19 | 20 | export default History 21 | -------------------------------------------------------------------------------- /examples/breadcrumbs/app.css: -------------------------------------------------------------------------------- 1 | aside { 2 | position: absolute; 3 | left: 0; 4 | width: 300px; 5 | } 6 | 7 | main { 8 | position: absolute; 9 | left: 310px; 10 | } 11 | 12 | ul.breadcrumbs-list { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | ul.breadcrumbs-list li { 18 | display: inline-block; 19 | margin-right: 20px; 20 | } 21 | 22 | ul.breadcrumbs-list li a:not(.breadcrumb-active) { 23 | font-weight: bold; 24 | margin-right: 20px; 25 | } 26 | 27 | ul.breadcrumbs-list li a.breadcrumb-active { 28 | text-decoration: none; 29 | cursor: default; 30 | } 31 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Grades/components/Grades.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React, { Component } from 'react' 3 | 4 | class Grades extends Component { 5 | render() { 6 | let { assignments } = COURSES[this.props.params.courseId] 7 | 8 | return ( 9 |
10 |

Grades

11 |
    12 | {assignments.map(assignment => ( 13 |
  • {assignment.grade} - {assignment.title}
  • 14 | ))} 15 |
16 |
17 | ) 18 | } 19 | } 20 | 21 | module.exports = Grades 22 | -------------------------------------------------------------------------------- /examples/auth-flow-async-with-query-params/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Authentication with query parameters 7 | 8 | 9 | 10 |

React Router Examples / Async Auth with Query Parameters

11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/guides/NavigatingOutsideOfComponents.md: -------------------------------------------------------------------------------- 1 | # Navigating Outside of Components 2 | 3 | While you can use `this.props.router` from `withRouter` to navigate around, many apps want to be able to navigate outside of their components. They can do that with the history the app gives to `Router`. 4 | 5 | ```js 6 | // your main file that renders a Router 7 | import { Router, browserHistory } from 'react-router' 8 | import routes from './app/routes' 9 | render(, el) 10 | ``` 11 | 12 | ```js 13 | // somewhere like a redux/flux action file: 14 | import { browserHistory } from 'react-router' 15 | browserHistory.push('/some/path') 16 | ``` 17 | -------------------------------------------------------------------------------- /modules/getRouteParams.js: -------------------------------------------------------------------------------- 1 | import { getParamNames } from './PatternUtils' 2 | 3 | /** 4 | * Extracts an object of params the given route cares about from 5 | * the given params object. 6 | */ 7 | function getRouteParams(route, params) { 8 | const routeParams = {} 9 | 10 | if (!route.path) 11 | return routeParams 12 | 13 | const paramNames = getParamNames(route.path) 14 | 15 | for (const p in params) { 16 | if ( 17 | Object.prototype.hasOwnProperty.call(params, p) && 18 | paramNames.indexOf(p) !== -1 19 | ) { 20 | routeParams[p] = params[p] 21 | } 22 | } 23 | 24 | return routeParams 25 | } 26 | 27 | export default getRouteParams 28 | -------------------------------------------------------------------------------- /modules/createMemoryHistory.js: -------------------------------------------------------------------------------- 1 | import useQueries from 'history/lib/useQueries' 2 | import useBasename from 'history/lib/useBasename' 3 | import baseCreateMemoryHistory from 'history/lib/createMemoryHistory' 4 | 5 | export default function createMemoryHistory(options) { 6 | // signatures and type checking differ between `useRoutes` and 7 | // `createMemoryHistory`, have to create `memoryHistory` first because 8 | // `useQueries` doesn't understand the signature 9 | const memoryHistory = baseCreateMemoryHistory(options) 10 | const createHistory = () => memoryHistory 11 | const history = useQueries(useBasename(createHistory))(options) 12 | history.__v2_compatible__ = true 13 | return history 14 | } 15 | -------------------------------------------------------------------------------- /modules/withRouter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import hoistStatics from 'hoist-non-react-statics' 3 | import { routerShape } from './PropTypes' 4 | 5 | function getDisplayName(WrappedComponent) { 6 | return WrappedComponent.displayName || WrappedComponent.name || 'Component' 7 | } 8 | 9 | export default function withRouter(WrappedComponent) { 10 | const WithRouter = React.createClass({ 11 | contextTypes: { router: routerShape }, 12 | render() { 13 | return 14 | } 15 | }) 16 | 17 | WithRouter.displayName = `withRouter(${getDisplayName(WrappedComponent)})` 18 | WithRouter.WrappedComponent = WrappedComponent 19 | 20 | return hoistStatics(WithRouter, WrappedComponent) 21 | } 22 | -------------------------------------------------------------------------------- /modules/RouterUtils.js: -------------------------------------------------------------------------------- 1 | import deprecateObjectProperties from './deprecateObjectProperties' 2 | 3 | export function createRouterObject(history, transitionManager) { 4 | return { 5 | ...history, 6 | setRouteLeaveHook: transitionManager.listenBeforeLeavingRoute, 7 | isActive: transitionManager.isActive 8 | } 9 | } 10 | 11 | // deprecated 12 | export function createRoutingHistory(history, transitionManager) { 13 | history = { 14 | ...history, 15 | ...transitionManager 16 | } 17 | 18 | if (__DEV__) { 19 | history = deprecateObjectProperties( 20 | history, 21 | '`props.history` and `context.history` are deprecated. Please use `context.router`. http://tiny.cc/router-contextchanges' 22 | ) 23 | } 24 | 25 | return history 26 | } 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | 5 | output: { 6 | library: 'ReactRouter', 7 | libraryTarget: 'umd' 8 | }, 9 | 10 | externals: [ 11 | { 12 | react: { 13 | root: 'React', 14 | commonjs2: 'react', 15 | commonjs: 'react', 16 | amd: 'react' 17 | } 18 | } 19 | ], 20 | 21 | module: { 22 | loaders: [ 23 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' } 24 | ] 25 | }, 26 | 27 | node: { 28 | Buffer: false 29 | }, 30 | 31 | plugins: [ 32 | new webpack.optimize.OccurenceOrderPlugin(), 33 | new webpack.DefinePlugin({ 34 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 35 | }) 36 | ] 37 | 38 | } 39 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Assignments/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React, { Component } from 'react' 3 | import { Link } from 'react-router' 4 | 5 | class Sidebar extends Component { 6 | render() { 7 | let { assignments } = COURSES[this.props.params.courseId] 8 | 9 | return ( 10 |
11 |

Sidebar Assignments

12 |
    13 | {assignments.map(assignment => ( 14 |
  • 15 | 16 | {assignment.title} 17 | 18 |
  • 19 | ))} 20 |
21 |
22 | ) 23 | } 24 | } 25 | 26 | module.exports = Sidebar 27 | -------------------------------------------------------------------------------- /modules/InternalPropTypes.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react' 2 | 3 | const { func, object, arrayOf, oneOfType, element, shape, string } = PropTypes 4 | 5 | export function falsy(props, propName, componentName) { 6 | if (props[propName]) 7 | return new Error(`<${componentName}> should not have a "${propName}" prop`) 8 | } 9 | 10 | export const history = shape({ 11 | listen: func.isRequired, 12 | push: func.isRequired, 13 | replace: func.isRequired, 14 | go: func.isRequired, 15 | goBack: func.isRequired, 16 | goForward: func.isRequired 17 | }) 18 | 19 | export const component = oneOfType([ func, string ]) 20 | export const components = oneOfType([ component, object ]) 21 | export const route = oneOfType([ object, element ]) 22 | export const routes = oneOfType([ route, arrayOf(route) ]) 23 | -------------------------------------------------------------------------------- /examples/master-detail/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Arial; 3 | font-weight: 200; 4 | } 5 | 6 | a { 7 | color: hsl(200, 50%, 50%); 8 | } 9 | 10 | a.active { 11 | color: hsl(20, 50%, 50%); 12 | } 13 | 14 | #example { 15 | position: absolute; 16 | } 17 | 18 | .App { 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | bottom: 0; 24 | width: 500px; 25 | height: 500px; 26 | } 27 | 28 | .ContactList { 29 | position: absolute; 30 | left: 0; 31 | top: 0; 32 | bottom: 0; 33 | width: 300px; 34 | overflow: auto; 35 | padding: 20px; 36 | } 37 | 38 | .Content { 39 | position: absolute; 40 | left: 300px; 41 | top: 0; 42 | bottom: 0; 43 | right: 0; 44 | border-left: 1px solid #ccc; 45 | overflow: auto; 46 | padding: 40px; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /modules/__tests__/getParamNames-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import { getParamNames } from '../PatternUtils' 3 | 4 | describe('getParamNames', function () { 5 | describe('when a pattern contains no dynamic segments', function () { 6 | it('returns an empty array', function () { 7 | expect(getParamNames('a/b/c')).toEqual([]) 8 | }) 9 | }) 10 | 11 | describe('when a pattern contains :a and :b dynamic segments', function () { 12 | it('returns the correct names', function () { 13 | expect(getParamNames('/comments/:a/:b/edit')).toEqual([ 'a', 'b' ]) 14 | }) 15 | }) 16 | 17 | describe('when a pattern has a *', function () { 18 | it('uses the name "splat"', function () { 19 | expect(getParamNames('/files/*.jpg')).toEqual([ 'splat' ]) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /examples/passing-props-to-children/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Arial; 3 | font-weight: 200; 4 | } 5 | 6 | a { 7 | color: hsl(200, 50%, 50%); 8 | } 9 | 10 | a.active { 11 | color: hsl(20, 50%, 50%); 12 | } 13 | 14 | #example { 15 | position: absolute; 16 | } 17 | 18 | .App { 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | bottom: 0; 24 | width: 500px; 25 | height: 500px; 26 | } 27 | 28 | .Master { 29 | position: absolute; 30 | left: 0; 31 | top: 0; 32 | bottom: 0; 33 | width: 300px; 34 | overflow: auto; 35 | padding: 10px 40px; 36 | } 37 | 38 | .Detail { 39 | position: absolute; 40 | left: 300px; 41 | top: 0; 42 | bottom: 0; 43 | right: 0; 44 | border-left: 1px solid #ccc; 45 | overflow: auto; 46 | padding: 40px; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/routes/Announcements/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React, { Component } from 'react' 3 | import { Link } from 'react-router' 4 | 5 | class AnnouncementsSidebar extends Component { 6 | render() { 7 | let { announcements } = COURSES[this.props.params.courseId] 8 | 9 | return ( 10 |
11 |

Sidebar Assignments

12 |
    13 | {announcements.map(announcement => ( 14 |
  • 15 | 16 | {announcement.title} 17 | 18 |
  • 19 | ))} 20 |
21 |
22 | ) 23 | } 24 | } 25 | 26 | module.exports = AnnouncementsSidebar 27 | -------------------------------------------------------------------------------- /examples/nested-animations/app.css: -------------------------------------------------------------------------------- 1 | .Image { 2 | position: absolute; 3 | height: 400px; 4 | width: 400px; 5 | } 6 | 7 | .swap-enter { 8 | opacity: 0.01; 9 | transform: translateX(5em); 10 | transition: all .5s ease-in; 11 | } 12 | 13 | .swap-enter.swap-enter-active { 14 | opacity: 1; 15 | transform: translateX(0); 16 | } 17 | 18 | .swap-leave { 19 | opacity: 1; 20 | transform: translateX(0); 21 | transition: all .5s ease-in; 22 | } 23 | 24 | .swap-leave.swap-leave-active { 25 | opacity: 0; 26 | transform: translateX(-5em); 27 | } 28 | 29 | .example-enter { 30 | opacity: 0.01; 31 | transition: opacity .5s ease-in; 32 | } 33 | 34 | .example-enter.example-enter-active { 35 | opacity: 1; 36 | } 37 | 38 | .example-leave { 39 | opacity: 1; 40 | transition: opacity .5s ease-in; 41 | } 42 | 43 | .example-leave.example-leave-active { 44 | opacity: 0; 45 | } 46 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-console, no-var */ 2 | var express = require('express') 3 | var rewrite = require('express-urlrewrite') 4 | var webpack = require('webpack') 5 | var webpackDevMiddleware = require('webpack-dev-middleware') 6 | var WebpackConfig = require('./webpack.config') 7 | 8 | var app = express() 9 | 10 | app.use(webpackDevMiddleware(webpack(WebpackConfig), { 11 | publicPath: '/__build__/', 12 | stats: { 13 | colors: true 14 | } 15 | })) 16 | 17 | var fs = require('fs') 18 | var path = require('path') 19 | 20 | fs.readdirSync(__dirname).forEach(function (file) { 21 | if (fs.statSync(path.join(__dirname, file)).isDirectory()) 22 | app.use(rewrite('/' + file + '/*', '/' + file + '/index.html')) 23 | }) 24 | 25 | app.use(express.static(__dirname)) 26 | 27 | app.listen(8080, function () { 28 | console.log('Server listening on http://localhost:8080, Ctrl+C to stop') 29 | }) 30 | -------------------------------------------------------------------------------- /docs/guides/ConfirmingNavigation.md: -------------------------------------------------------------------------------- 1 | # Confirming Navigation 2 | 3 | You can prevent a transition from happening or prompt the user before leaving a [route](/docs/Glossary.md#route) with a leave hook. 4 | 5 | ```js 6 | const Home = withRouter( 7 | React.createClass({ 8 | 9 | componentDidMount() { 10 | this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave) 11 | }, 12 | 13 | routerWillLeave(nextLocation) { 14 | // return false to prevent a transition w/o prompting the user, 15 | // or return a string to allow the user to decide: 16 | if (!this.state.isSaved) 17 | return 'Your work is not saved! Are you sure you want to leave?' 18 | }, 19 | 20 | // ... 21 | 22 | }) 23 | ) 24 | ``` 25 | 26 | Note that this example makes use of the [withRouter](https://github.com/reactjs/react-router/blob/v2.4.0/upgrade-guides/v2.4.0.md) higher-order component introduced in v2.4.0. 27 | -------------------------------------------------------------------------------- /modules/RouteContext.js: -------------------------------------------------------------------------------- 1 | import warning from './routerWarning' 2 | import React from 'react' 3 | 4 | const { object } = React.PropTypes 5 | 6 | /** 7 | * The RouteContext mixin provides a convenient way for route 8 | * components to set the route in context. This is needed for 9 | * routes that render elements that want to use the Lifecycle 10 | * mixin to prevent transitions. 11 | */ 12 | const RouteContext = { 13 | 14 | propTypes: { 15 | route: object.isRequired 16 | }, 17 | 18 | childContextTypes: { 19 | route: object.isRequired 20 | }, 21 | 22 | getChildContext() { 23 | return { 24 | route: this.props.route 25 | } 26 | }, 27 | 28 | componentWillMount() { 29 | warning(false, 'The `RouteContext` mixin is deprecated. You can provide `this.props.route` on context with your own `contextTypes`. http://tiny.cc/router-routecontextmixin') 30 | } 31 | 32 | } 33 | 34 | export default RouteContext 35 | -------------------------------------------------------------------------------- /modules/__tests__/IndexRedirect-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import { render, unmountComponentAtNode } from 'react-dom' 4 | import createHistory from '../createMemoryHistory' 5 | import IndexRedirect from '../IndexRedirect' 6 | import Router from '../Router' 7 | import Route from '../Route' 8 | 9 | describe('An ', function () { 10 | 11 | let node 12 | beforeEach(function () { 13 | node = document.createElement('div') 14 | }) 15 | 16 | afterEach(function () { 17 | unmountComponentAtNode(node) 18 | }) 19 | 20 | it('works', function (done) { 21 | render(( 22 | 23 | 24 | 25 | 26 | 27 | 28 | ), node, function () { 29 | expect(this.state.location.pathname).toEqual('/messages') 30 | done() 31 | }) 32 | }) 33 | 34 | }) 35 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | * [Tutorial](https://github.com/reactjs/react-router-tutorial) 4 | * [Introduction](Introduction.md) 5 | * Basics 6 | * [Route Configuration](guides/RouteConfiguration.md) 7 | * [Route Matching](guides/RouteMatching.md) 8 | * [Histories](guides/Histories.md) 9 | * [Index Routes and Links](guides/IndexRoutes.md) 10 | * Advanced Usage 11 | * [Dynamic Routing](guides/DynamicRouting.md) 12 | * [Confirming Navigation](guides/ConfirmingNavigation.md) 13 | * [Server Rendering](guides/ServerRendering.md) 14 | * [Component Lifecycle](guides/ComponentLifecycle.md) 15 | * [Navigating Outside of Components](guides/NavigatingOutsideOfComponents.md) 16 | * [Minimizing Bundle Size](guides/MinimizingBundleSize.md) 17 | * [Change Log](/CHANGES.md) 18 | * [Upgrading to v1.0.0](../upgrade-guides/v1.0.0.md) 19 | * [Upgrading to v2.0.0](../upgrade-guides/v2.0.0.md) 20 | * [Troubleshooting](Troubleshooting.md) 21 | * [API](API.md) 22 | * [Glossary](Glossary.md) 23 | -------------------------------------------------------------------------------- /examples/huge-apps/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | class Dashboard extends Component { 5 | render() { 6 | const { courses } = this.props 7 | 8 | return ( 9 |
10 |

Super Scalable Apps

11 |

12 | Open the network tab as you navigate. Notice that only the amount of 13 | your app that is required is actually downloaded as you navigate 14 | around. Even the route configuration objects are loaded on the fly. 15 | This way, a new route added deep in your app will not affect the 16 | initial bundle of your application. 17 |

18 |

Courses

{' '} 19 |
    20 | {courses.map(course => ( 21 |
  • 22 | {course.name} 23 |
  • 24 | ))} 25 |
26 |
27 | ) 28 | } 29 | } 30 | 31 | export default Dashboard 32 | -------------------------------------------------------------------------------- /modules/applyRouterMiddleware.js: -------------------------------------------------------------------------------- 1 | import React, { createElement } from 'react' 2 | import RouterContext from './RouterContext' 3 | 4 | export default (...middlewares) => { 5 | const withContext = middlewares.map(m => m.renderRouterContext).filter(f => f) 6 | const withComponent = middlewares.map(m => m.renderRouteComponent).filter(f => f) 7 | const makeCreateElement = (baseCreateElement = createElement) => ( 8 | (Component, props) => ( 9 | withComponent.reduceRight( 10 | (previous, renderRouteComponent) => ( 11 | renderRouteComponent(previous, props) 12 | ), baseCreateElement(Component, props) 13 | ) 14 | ) 15 | ) 16 | 17 | return (renderProps) => ( 18 | withContext.reduceRight( 19 | (previous, renderRouterContext) => ( 20 | renderRouterContext(previous, renderProps) 21 | ), ( 22 | 26 | ) 27 | ) 28 | ) 29 | } 30 | 31 | -------------------------------------------------------------------------------- /upgrade-guides/v2.2.0.md: -------------------------------------------------------------------------------- 1 | # v2.2.0 Upgrade Guide 2 | 3 | ## `getComponent`, `getComponents` signature 4 | 5 | **This is unlikely to affect you, even if you use `getComponent` and `getComponents`.** 6 | 7 | The signature of `getComponent` and `getComponents` has been changed from `(location: Location, callback: Function) => any` to `(nextState: RouterState, callback: Function) => any`. That means that instead of writing 8 | 9 | ```js 10 | getComponent(location, cb) { 11 | cb(fetchComponent(location.query)) 12 | } 13 | ``` 14 | 15 | You would need to instead write 16 | 17 | ```js 18 | getComponent(nextState, cb) { 19 | cb(fetchComponent(nextState.location.query)) 20 | } 21 | ``` 22 | 23 | However, you now also have access to the matched `params` on `nextState`, and can use those to determine which component to return. 24 | 25 | You will still be able to access location properties directly on `nextState` until the next breaking release (and in fact they will shadow router state properties, if applicable), but this will case a deprecation warning in development mode. 26 | -------------------------------------------------------------------------------- /modules/useRoutes.js: -------------------------------------------------------------------------------- 1 | import useQueries from 'history/lib/useQueries' 2 | 3 | import createTransitionManager from './createTransitionManager' 4 | import warning from './routerWarning' 5 | 6 | /** 7 | * Returns a new createHistory function that may be used to create 8 | * history objects that know about routing. 9 | * 10 | * Enhances history objects with the following methods: 11 | * 12 | * - listen((error, nextState) => {}) 13 | * - listenBeforeLeavingRoute(route, (nextLocation) => {}) 14 | * - match(location, (error, redirectLocation, nextState) => {}) 15 | * - isActive(pathname, query, indexOnly=false) 16 | */ 17 | function useRoutes(createHistory) { 18 | warning( 19 | false, 20 | '`useRoutes` is deprecated. Please use `createTransitionManager` instead.' 21 | ) 22 | 23 | return function ({ routes, ...options } = {}) { 24 | const history = useQueries(createHistory)(options) 25 | const transitionManager = createTransitionManager(history, routes) 26 | return { ...history, ...transitionManager } 27 | } 28 | } 29 | 30 | export default useRoutes 31 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-env mocha */ 3 | 4 | import expect from 'expect' 5 | 6 | import { _resetWarned } from './modules/routerWarning' 7 | 8 | beforeEach(() => { 9 | expect.spyOn(console, 'error').andCall(msg => { 10 | for (const about of console.error.expected) { 11 | if (msg.indexOf(about) !== -1) { 12 | console.error.warned[about] = true 13 | return 14 | } 15 | } 16 | 17 | console.error.threw = true 18 | throw new Error(msg) 19 | }) 20 | 21 | console.error.expected = [] 22 | console.error.warned = Object.create(null) 23 | console.error.threw = false 24 | }) 25 | 26 | afterEach(() => { 27 | if (!console.error.threw) { 28 | console.error.expected.forEach(about => { 29 | expect(console.error.warned[about]).toExist( 30 | `Missing expected warning: ${about}` 31 | ) 32 | }) 33 | } 34 | 35 | console.error.restore() 36 | _resetWarned() 37 | }) 38 | 39 | const context = require.context('./modules', true, /-test\.js$/) 40 | context.keys().forEach(context) 41 | -------------------------------------------------------------------------------- /docs/guides/MinimizingBundleSize.md: -------------------------------------------------------------------------------- 1 | # Minimizing Bundle Size 2 | 3 | For convenience, React Router exposes its full API on the top-level `react-router` import. However, this causes the entire React Router library and its dependencies to be included in client bundles that include code that imports from the top-level CommonJS bundle. 4 | 5 | Instead, the bindings exported from `react-router` are also available in `react-router/lib`. When using CommonJS modules, you can import directly from `react-router/lib` to avoid pulling in unused modules. 6 | 7 | Assuming you are transpiling ES2015 modules into CommonJS modules, instead of: 8 | 9 | ```js 10 | import { Link, Route, Router } from 'react-router' 11 | ``` 12 | 13 | use: 14 | 15 | ```js 16 | import Link from 'react-router/lib/Link' 17 | import Route from 'react-router/lib/Route' 18 | import Router from 'react-router/lib/Router' 19 | ``` 20 | 21 | The public API available in this manner is defined as the set of imports available from the top-level `react-router` module. Anything not available through the top-level `react-router` module is a private API, and is subject to change without notice. 22 | -------------------------------------------------------------------------------- /modules/__tests__/withRouter-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React, { Component } from 'react' 3 | import { render, unmountComponentAtNode } from 'react-dom' 4 | import createHistory from '../createMemoryHistory' 5 | import Route from '../Route' 6 | import Router from '../Router' 7 | import routerShape from '../PropTypes' 8 | import withRouter from '../withRouter' 9 | 10 | describe('withRouter', function () { 11 | class App extends Component { 12 | propTypes: { 13 | router: routerShape.isRequired 14 | } 15 | render() { 16 | expect(this.props.router).toExist() 17 | return

App

18 | } 19 | } 20 | 21 | let node 22 | beforeEach(function () { 23 | node = document.createElement('div') 24 | }) 25 | 26 | afterEach(function () { 27 | unmountComponentAtNode(node) 28 | }) 29 | 30 | it('puts router on context', function (done) { 31 | const WrappedApp = withRouter(App) 32 | 33 | render(( 34 | 35 | 36 | 37 | ), node, function () { 38 | done() 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ryan Florence, Michael Jackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | export RELEASE=1 3 | 4 | if ! [ -e scripts/release.sh ]; then 5 | echo >&2 "Please run scripts/release.sh from the repo root" 6 | exit 1 7 | fi 8 | 9 | update_version() { 10 | echo "$(node -p "p=require('./${1}');p.version='${2}';JSON.stringify(p,null,2)")" > $1 11 | echo "Updated ${1} version to ${2}" 12 | } 13 | 14 | validate_semver() { 15 | if ! [[ $1 =~ ^[0-9]\.[0-9]+\.[0-9](-.+)? ]]; then 16 | echo >&2 "Version $1 is not valid! It must be a valid semver string like 1.0.2 or 2.3.0-beta.1" 17 | exit 1 18 | fi 19 | } 20 | 21 | current_version=$(node -p "require('./package').version") 22 | 23 | printf "Next version (current is $current_version)? " 24 | read next_version 25 | 26 | validate_semver $next_version 27 | 28 | next_ref="v$next_version" 29 | 30 | npm test 31 | 32 | update_version 'package.json' $next_version 33 | 34 | git commit -am "Version $next_version" 35 | 36 | # push first to make sure we're up-to-date 37 | git push origin master 38 | 39 | git tag $next_ref 40 | git tag latest -f 41 | 42 | git push origin $next_ref 43 | git push origin latest -f 44 | 45 | node scripts/build.js 46 | 47 | npm publish 48 | -------------------------------------------------------------------------------- /modules/index.js: -------------------------------------------------------------------------------- 1 | /* components */ 2 | export Router from './Router' 3 | export Link from './Link' 4 | export IndexLink from './IndexLink' 5 | export withRouter from './withRouter' 6 | 7 | /* components (configuration) */ 8 | export IndexRedirect from './IndexRedirect' 9 | export IndexRoute from './IndexRoute' 10 | export Redirect from './Redirect' 11 | export Route from './Route' 12 | 13 | /* mixins */ 14 | export History from './History' 15 | export Lifecycle from './Lifecycle' 16 | export RouteContext from './RouteContext' 17 | 18 | /* utils */ 19 | export useRoutes from './useRoutes' 20 | export { createRoutes } from './RouteUtils' 21 | export RouterContext from './RouterContext' 22 | export RoutingContext from './RoutingContext' 23 | export PropTypes, { locationShape, routerShape } from './PropTypes' 24 | export match from './match' 25 | export useRouterHistory from './useRouterHistory' 26 | export { formatPattern } from './PatternUtils' 27 | export applyRouterMiddleware from './applyRouterMiddleware' 28 | 29 | /* histories */ 30 | export browserHistory from './browserHistory' 31 | export hashHistory from './hashHistory' 32 | export createMemoryHistory from './createMemoryHistory' 33 | -------------------------------------------------------------------------------- /examples/auth-flow/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | login(email, pass, cb) { 3 | cb = arguments[arguments.length - 1] 4 | if (localStorage.token) { 5 | if (cb) cb(true) 6 | this.onChange(true) 7 | return 8 | } 9 | pretendRequest(email, pass, (res) => { 10 | if (res.authenticated) { 11 | localStorage.token = res.token 12 | if (cb) cb(true) 13 | this.onChange(true) 14 | } else { 15 | if (cb) cb(false) 16 | this.onChange(false) 17 | } 18 | }) 19 | }, 20 | 21 | getToken() { 22 | return localStorage.token 23 | }, 24 | 25 | logout(cb) { 26 | delete localStorage.token 27 | if (cb) cb() 28 | this.onChange(false) 29 | }, 30 | 31 | loggedIn() { 32 | return !!localStorage.token 33 | }, 34 | 35 | onChange() {} 36 | } 37 | 38 | function pretendRequest(email, pass, cb) { 39 | setTimeout(() => { 40 | if (email === 'joe@example.com' && pass === 'password1') { 41 | cb({ 42 | authenticated: true, 43 | token: Math.random().toString(36).substring(7) 44 | }) 45 | } else { 46 | cb({ authenticated: false }) 47 | } 48 | }, 0) 49 | } 50 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-var */ 2 | var fs = require('fs') 3 | var path = require('path') 4 | var webpack = require('webpack') 5 | 6 | module.exports = { 7 | 8 | devtool: 'inline-source-map', 9 | 10 | entry: fs.readdirSync(__dirname).reduce(function (entries, dir) { 11 | if (fs.statSync(path.join(__dirname, dir)).isDirectory()) 12 | entries[dir] = path.join(__dirname, dir, 'app.js') 13 | 14 | return entries 15 | }, {}), 16 | 17 | output: { 18 | path: __dirname + '/__build__', 19 | filename: '[name].js', 20 | chunkFilename: '[id].chunk.js', 21 | publicPath: '/__build__/' 22 | }, 23 | 24 | module: { 25 | loaders: [ 26 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' }, 27 | { test: /\.css$/, loader: 'style!css' } 28 | ] 29 | }, 30 | 31 | resolve: { 32 | alias: { 33 | 'react-router': path.join(__dirname, '..', 'modules') 34 | } 35 | }, 36 | 37 | plugins: [ 38 | new webpack.optimize.CommonsChunkPlugin('shared.js'), 39 | new webpack.DefinePlugin({ 40 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 41 | }) 42 | ] 43 | 44 | } 45 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | const styles = {} 5 | 6 | styles.nav = { 7 | borderBottom: '1px solid #aaa' 8 | } 9 | 10 | styles.link = { 11 | display: 'inline-block', 12 | padding: 10, 13 | textDecoration: 'none' 14 | } 15 | 16 | styles.activeLink = { 17 | ...styles.link, 18 | color: 'red' 19 | } 20 | 21 | class Nav extends Component { 22 | render() { 23 | const { course } = this.props 24 | const pages = [ 25 | [ 'announcements', 'Announcements' ], 26 | [ 'assignments', 'Assignments' ], 27 | [ 'grades', 'Grades' ] 28 | ] 29 | 30 | return ( 31 | 41 | ) 42 | } 43 | } 44 | 45 | export default Nav 46 | -------------------------------------------------------------------------------- /examples/huge-apps/stubs/COURSES.js: -------------------------------------------------------------------------------- 1 | global.COURSES = [ 2 | { 3 | id: 0, 4 | name: 'React Fundamentals', 5 | grade: 'B', 6 | announcements: [ 7 | { 8 | id: 0, 9 | title: 'No class tomorrow', 10 | body: 'There is no class tomorrow, please do not show up' 11 | } 12 | ], 13 | assignments: [ 14 | { 15 | id: 0, 16 | title: 'Build a router', 17 | body: 'It will be easy, seriously, like 2 hours, 100 lines of code, no biggie', 18 | grade: 'N/A' 19 | } 20 | ] 21 | 22 | }, 23 | 24 | { 25 | id: 1, 26 | name: 'Reusable React Components', 27 | grade: 'A-', 28 | announcements: [ 29 | { 30 | id: 0, 31 | title: 'Final exam next wednesday', 32 | body: 'You had better prepare' 33 | } 34 | ], 35 | assignments: [ 36 | { 37 | id: 0, 38 | title: 'PropTypes', 39 | body: 'They aren\'t for you.', 40 | grade: '80%' 41 | }, 42 | { 43 | id: 1, 44 | title: 'Iterating and Cloning Children', 45 | body: 'You can totally do it.', 46 | grade: '95%' 47 | } 48 | ] 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/components/Course.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React, { Component } from 'react' 3 | import Dashboard from './Dashboard' 4 | import Nav from './Nav' 5 | 6 | const styles = {} 7 | 8 | styles.sidebar = { 9 | float: 'left', 10 | width: 200, 11 | padding: 20, 12 | borderRight: '1px solid #aaa', 13 | marginRight: 20 14 | } 15 | 16 | class Course extends Component { 17 | render() { 18 | let { sidebar, main, children, params } = this.props 19 | let course = COURSES[params.courseId] 20 | 21 | let content 22 | if (sidebar && main) { 23 | content = ( 24 |
25 |
26 | {sidebar} 27 |
28 |
29 | {main} 30 |
31 |
32 | ) 33 | } else if (children) { 34 | content = children 35 | } else { 36 | content = 37 | } 38 | 39 | return ( 40 |
41 |

{course.name}

42 |
45 | ) 46 | } 47 | } 48 | 49 | module.exports = Course 50 | -------------------------------------------------------------------------------- /modules/__tests__/_bc-History-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import { render, unmountComponentAtNode } from 'react-dom' 4 | import History from '../History' 5 | import Router from '../Router' 6 | import Route from '../Route' 7 | import createHistory from 'history/lib/createMemoryHistory' 8 | import shouldWarn from './shouldWarn' 9 | 10 | describe('v1 History Mixin', function () { 11 | 12 | let node 13 | beforeEach(function () { 14 | node = document.createElement('div') 15 | }) 16 | 17 | afterEach(function () { 18 | unmountComponentAtNode(node) 19 | }) 20 | 21 | beforeEach(function () { 22 | shouldWarn('deprecated') 23 | }) 24 | 25 | it('assigns the history to the component instance', function (done) { 26 | 27 | const history = createHistory('/') 28 | 29 | const Component = React.createClass({ 30 | mixins: [ History ], 31 | componentWillMount() { 32 | expect(this.history).toExist() 33 | }, 34 | render() { return null } 35 | }) 36 | 37 | render(( 38 | 39 | 40 | 41 | ), node, done) 42 | }) 43 | 44 | }) 45 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | import auth from '../utils/auth' 4 | 5 | const App = React.createClass({ 6 | 7 | getInitialState() { 8 | return { 9 | loggedIn: auth.loggedIn() 10 | } 11 | }, 12 | 13 | updateAuth(loggedIn) { 14 | this.setState({ 15 | loggedIn: !!loggedIn 16 | }) 17 | }, 18 | 19 | componentWillMount() { 20 | auth.onChange = this.updateAuth 21 | auth.login() 22 | }, 23 | 24 | render() { 25 | return ( 26 |
27 |
    28 |
  • 29 | {this.state.loggedIn ? ( 30 | Log out 31 | ) : ( 32 | Sign in 33 | )} 34 |
  • 35 |
  • About
  • 36 |
  • Home (changes depending on auth status)
  • 37 |
  • Page Two (authenticated)
  • 38 |
  • User: Foo (authenticated)
  • 39 |
40 | {this.props.children} 41 |
42 | ) 43 | } 44 | 45 | }) 46 | 47 | export default App 48 | -------------------------------------------------------------------------------- /modules/__tests__/RouteComponent-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React, { Component } from 'react' 3 | import { render, unmountComponentAtNode } from 'react-dom' 4 | import createHistory from '../createMemoryHistory' 5 | import Router from '../Router' 6 | 7 | describe('a Route Component', function () { 8 | 9 | let node 10 | beforeEach(function () { 11 | node = document.createElement('div') 12 | }) 13 | 14 | afterEach(function () { 15 | unmountComponentAtNode(node) 16 | }) 17 | 18 | it('injects the right props', function (done) { 19 | class Parent extends Component { 20 | componentDidMount() { 21 | expect(this.props.route).toEqual(parent) 22 | expect(this.props.routes).toEqual([ parent, child ]) 23 | } 24 | render() { 25 | return null 26 | } 27 | } 28 | 29 | class Child extends Component { 30 | render() { 31 | return null 32 | } 33 | } 34 | 35 | const child = { path: 'child', component: Child } 36 | const parent = { path: '/', component: Parent, childRoutes: [ child ] } 37 | 38 | render(( 39 | 40 | ), node, done) 41 | }) 42 | 43 | }) 44 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/utils/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | login(email, pass, cb) { 3 | cb = arguments[arguments.length - 1] 4 | if (localStorage.token) { 5 | if (cb) cb(true) 6 | this.onChange(true) 7 | return 8 | } 9 | pretendRequest(email, pass, (res) => { 10 | if (res.authenticated) { 11 | localStorage.token = res.token 12 | if (cb) cb(true) 13 | this.onChange(true) 14 | } else { 15 | if (cb) cb(false) 16 | this.onChange(false) 17 | } 18 | }) 19 | }, 20 | 21 | getToken: function () { 22 | return localStorage.token 23 | }, 24 | 25 | logout: function (cb) { 26 | delete localStorage.token 27 | if (cb) cb() 28 | this.onChange(false) 29 | }, 30 | 31 | loggedIn: function () { 32 | return !!localStorage.token 33 | }, 34 | 35 | onChange: function () {} 36 | } 37 | 38 | function pretendRequest(email, pass, cb) { 39 | setTimeout(() => { 40 | if (email === 'joe@example.com' && pass === 'password1') { 41 | cb({ 42 | authenticated: true, 43 | token: Math.random().toString(36).substring(7) 44 | }) 45 | } else { 46 | cb({ authenticated: false }) 47 | } 48 | }, 0) 49 | } 50 | -------------------------------------------------------------------------------- /examples/query-params/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link } from 'react-router' 4 | 5 | class User extends Component { 6 | render() { 7 | let { userID } = this.props.params 8 | let { query } = this.props.location 9 | let age = query && query.showAge ? '33' : '' 10 | 11 | return ( 12 |
13 |

User id: {userID}

14 | {age} 15 |
16 | ) 17 | } 18 | } 19 | 20 | class App extends Component { 21 | render() { 22 | return ( 23 |
24 |
    25 |
  • Bob
  • 26 |
  • Bob With Query Params
  • 27 |
  • Sally
  • 28 |
29 | {this.props.children} 30 |
31 | ) 32 | } 33 | } 34 | 35 | render(( 36 | 37 | 38 | 39 | 40 | 41 | ), document.getElementById('example')) 42 | -------------------------------------------------------------------------------- /modules/Route.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import invariant from 'invariant' 3 | import { createRouteFromReactElement } from './RouteUtils' 4 | import { component, components } from './InternalPropTypes' 5 | 6 | const { string, func } = React.PropTypes 7 | 8 | /** 9 | * A is used to declare which components are rendered to the 10 | * page when the URL matches a given pattern. 11 | * 12 | * Routes are arranged in a nested tree structure. When a new URL is 13 | * requested, the tree is searched depth-first to find a route whose 14 | * path matches the URL. When one is found, all routes in the tree 15 | * that lead to it are considered "active" and their components are 16 | * rendered into the DOM, nested in the same order as in the tree. 17 | */ 18 | const Route = React.createClass({ 19 | 20 | statics: { 21 | createRouteFromReactElement 22 | }, 23 | 24 | propTypes: { 25 | path: string, 26 | component, 27 | components, 28 | getComponent: func, 29 | getComponents: func 30 | }, 31 | 32 | /* istanbul ignore next: sanity check */ 33 | render() { 34 | invariant( 35 | false, 36 | ' elements are for router configuration only and should not be rendered' 37 | ) 38 | } 39 | 40 | }) 41 | 42 | export default Route 43 | -------------------------------------------------------------------------------- /modules/IndexRedirect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import warning from './routerWarning' 3 | import invariant from 'invariant' 4 | import Redirect from './Redirect' 5 | import { falsy } from './InternalPropTypes' 6 | 7 | const { string, object } = React.PropTypes 8 | 9 | /** 10 | * An is used to redirect from an indexRoute. 11 | */ 12 | const IndexRedirect = React.createClass({ 13 | 14 | statics: { 15 | 16 | createRouteFromReactElement(element, parentRoute) { 17 | /* istanbul ignore else: sanity check */ 18 | if (parentRoute) { 19 | parentRoute.indexRoute = Redirect.createRouteFromReactElement(element) 20 | } else { 21 | warning( 22 | false, 23 | 'An does not make sense at the root of your route config' 24 | ) 25 | } 26 | } 27 | 28 | }, 29 | 30 | propTypes: { 31 | to: string.isRequired, 32 | query: object, 33 | state: object, 34 | onEnter: falsy, 35 | children: falsy 36 | }, 37 | 38 | /* istanbul ignore next: sanity check */ 39 | render() { 40 | invariant( 41 | false, 42 | ' elements are for router configuration only and should not be rendered' 43 | ) 44 | } 45 | 46 | }) 47 | 48 | export default IndexRedirect 49 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Router Examples 5 | 6 | 7 | 8 |

React Router Examples

9 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /modules/IndexRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import warning from './routerWarning' 3 | import invariant from 'invariant' 4 | import { createRouteFromReactElement } from './RouteUtils' 5 | import { component, components, falsy } from './InternalPropTypes' 6 | 7 | const { func } = React.PropTypes 8 | 9 | /** 10 | * An is used to specify its parent's in 11 | * a JSX route config. 12 | */ 13 | const IndexRoute = React.createClass({ 14 | 15 | statics: { 16 | 17 | createRouteFromReactElement(element, parentRoute) { 18 | /* istanbul ignore else: sanity check */ 19 | if (parentRoute) { 20 | parentRoute.indexRoute = createRouteFromReactElement(element) 21 | } else { 22 | warning( 23 | false, 24 | 'An does not make sense at the root of your route config' 25 | ) 26 | } 27 | } 28 | 29 | }, 30 | 31 | propTypes: { 32 | path: falsy, 33 | component, 34 | components, 35 | getComponent: func, 36 | getComponents: func 37 | }, 38 | 39 | /* istanbul ignore next: sanity check */ 40 | render() { 41 | invariant( 42 | false, 43 | ' elements are for router configuration only and should not be rendered' 44 | ) 45 | } 46 | 47 | }) 48 | 49 | export default IndexRoute 50 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/components/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withRouter } from 'react-router' 3 | import auth from '../utils/auth.js' 4 | 5 | const Login = React.createClass({ 6 | getInitialState() { 7 | return { 8 | error: false 9 | } 10 | }, 11 | 12 | handleSubmit(event) { 13 | event.preventDefault() 14 | 15 | const email = this.refs.email.value 16 | const pass = this.refs.pass.value 17 | 18 | auth.login(email, pass, (loggedIn) => { 19 | if (!loggedIn) 20 | return this.setState({ error: true }) 21 | 22 | const { location } = this.props 23 | 24 | if (location.state && location.state.nextPathname) { 25 | this.props.router.replace(location.state.nextPathname) 26 | } else { 27 | this.props.router.replace('/') 28 | } 29 | }) 30 | }, 31 | 32 | render() { 33 | return ( 34 |
35 | 36 | (hint: password1)
37 | 38 | {this.state.error && ( 39 |

Bad login information

40 | )} 41 |
42 | ) 43 | } 44 | 45 | }) 46 | 47 | export default withRouter(Login) 48 | -------------------------------------------------------------------------------- /upgrade-guides/v2.4.0.md: -------------------------------------------------------------------------------- 1 | # v2.4.0 Upgrade Guide 2 | 3 | ## `withRouter` HoC (higher-order component) 4 | 5 | Prior to 2.4.0, you could access the `router` object via [`this.context`](https://facebook.github.io/react/docs/context.html). This is still true, but `context` is often times a difficult and error-prone API to work with. 6 | 7 | In order to more easily access the `router` object, a `withRouter` higher-order component has been added as the new primary means of access. As with other HoCs, it is usable on any React Component of any type (`React.createClass`, ES2015 `React.Component` classes, stateless functional components). 8 | 9 | ```js 10 | import React from 'react' 11 | import { withRouter } from 'react-router' 12 | 13 | const Page = React.createClass({ 14 | componentDidMount() { 15 | this.props.router.setRouteLeaveHook(this.props.route, () => { 16 | if (this.state.unsaved) 17 | return 'You have unsaved information, are you sure you want to leave this page?' 18 | }) 19 | }, 20 | 21 | render() { 22 | return
Stuff
23 | } 24 | }) 25 | 26 | export default withRouter(Page) 27 | ``` 28 | 29 | **It's important to note this is not a deprecation of the `context` API.** As long as React supports `this.context` in its current form, any code written for that API will continue to work. We will continue to use it internally and you can continue to write in that format, if you want. We think this new HoC is nicer and easier, and will be using it in documentation and examples, but it is not a hard requirement to switch. 30 | -------------------------------------------------------------------------------- /examples/route-no-match/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link } from 'react-router' 4 | 5 | class User extends React.Component { 6 | render() { 7 | let { userID } = this.props.params 8 | let { query } = this.props.location 9 | let age = query && query.showAge ? '33' : '' 10 | 11 | return ( 12 |
13 |

User id: {userID}

14 | {age} 15 |
16 | ) 17 | } 18 | } 19 | 20 | class App extends React.Component { 21 | render() { 22 | return ( 23 |
24 |
    25 |
  • Bob
  • 26 |
  • Bob With Query Params
  • 27 |
  • Sally
  • 28 |
29 | {this.props.children} 30 |
31 | ) 32 | } 33 | } 34 | 35 | class PageNotFound extends React.Component { 36 | render() { 37 | return ( 38 |
39 |

Page Not Found.

40 |

Go to Home Page

41 |
42 | ) 43 | } 44 | } 45 | 46 | render(( 47 | 48 | 49 | 50 | 51 | 52 | 53 | ), document.getElementById('example')) 54 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | ## Version 27 | 2.0.0 28 | 29 | ## Test Case 30 | http://jsbin.com/sacerobuxi/edit?html,js,output 31 | 32 | ## Steps to reproduce 33 | 34 | ## Expected Behavior 35 | 36 | ## Actual Behavior 37 | 38 | -------------------------------------------------------------------------------- /modules/__tests__/matchPattern-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import { matchPattern } from '../PatternUtils' 3 | 4 | describe('matchPattern', function () { 5 | 6 | function assertMatch(pattern, pathname, remainingPathname, paramNames, paramValues) { 7 | expect(matchPattern(pattern, pathname)).toEqual({ 8 | remainingPathname, 9 | paramNames, 10 | paramValues 11 | }) 12 | } 13 | 14 | it('works without params', function () { 15 | assertMatch('/', '/path', '/path', [], []) 16 | }) 17 | 18 | it('works with named params', function () { 19 | assertMatch('/:id', '/path', '', [ 'id' ], [ 'path' ]) 20 | assertMatch('/:id.:ext', '/path.jpg', '', [ 'id', 'ext' ], [ 'path', 'jpg' ]) 21 | }) 22 | 23 | it('works with named params that contain spaces', function () { 24 | assertMatch('/:id', '/path+more', '', [ 'id' ], [ 'path+more' ]) 25 | assertMatch('/:id', '/path%20more', '', [ 'id' ], [ 'path more' ]) 26 | }) 27 | 28 | it('works with splat params', function () { 29 | assertMatch('/files/*.*', '/files/path.jpg', '', [ 'splat', 'splat' ], [ 'path', 'jpg' ]) 30 | }) 31 | 32 | it('ignores trailing slashes', function () { 33 | assertMatch('/:id', '/path/', '', [ 'id' ], [ 'path' ]) 34 | }) 35 | 36 | it('works with greedy splat (**)', function () { 37 | assertMatch('/**/g', '/greedy/is/good/g', '', [ 'splat' ], [ 'greedy/is/good' ]) 38 | }) 39 | 40 | it('works with greedy and non-greedy splat', function () { 41 | assertMatch('/**/*.jpg', '/files/path/to/file.jpg', '', [ 'splat', 'splat' ], [ 'files/path/to', 'file' ]) 42 | }) 43 | 44 | }) 45 | -------------------------------------------------------------------------------- /modules/__tests__/AsyncUtils-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import { loopAsync, mapAsync } from '../AsyncUtils' 3 | 4 | describe('loopAsync', function () { 5 | it('should support calling done() and then next()', function (done) { 6 | const callback = (turn, next, done) => { 7 | done('foo') 8 | next() 9 | } 10 | 11 | const callbackSpy = expect.createSpy().andCall(callback) 12 | const doneSpy = expect.createSpy() 13 | 14 | loopAsync(10, callbackSpy, doneSpy) 15 | setTimeout(function () { 16 | expect(callbackSpy.calls.length).toBe(1) 17 | expect(doneSpy.calls.length).toBe(1) 18 | 19 | expect(doneSpy).toHaveBeenCalledWith('foo') 20 | 21 | done() 22 | }) 23 | }) 24 | }) 25 | 26 | 27 | describe('mapAsync', function () { 28 | it('should support zero-length inputs', function (done) { 29 | mapAsync( 30 | [], 31 | () => null, 32 | (_, values) => { 33 | expect(values).toEqual([]) 34 | done() 35 | } 36 | ) 37 | }) 38 | 39 | it('should only invoke callback once on multiple errors', function (done) { 40 | const error = new Error() 41 | const work = (item, index, callback) => { 42 | callback(error) 43 | } 44 | 45 | const workSpy = expect.createSpy().andCall(work) 46 | const doneSpy = expect.createSpy() 47 | 48 | mapAsync([ null, null, null ], workSpy, doneSpy) 49 | setTimeout(function () { 50 | expect(workSpy.calls.length).toBe(3) 51 | expect(doneSpy.calls.length).toBe(1) 52 | 53 | expect(doneSpy).toHaveBeenCalledWith(error) 54 | 55 | done() 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /modules/__tests__/push-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React, { Component } from 'react' 3 | import { render, unmountComponentAtNode } from 'react-dom' 4 | import createHistory from '../createMemoryHistory' 5 | import resetHash from './resetHash' 6 | import execSteps from './execSteps' 7 | import Router from '../Router' 8 | import Route from '../Route' 9 | 10 | describe('pushState', function () { 11 | 12 | class Index extends Component { 13 | render() { 14 | return

Index

15 | } 16 | } 17 | 18 | class Home extends Component { 19 | render() { 20 | return

Home

21 | } 22 | } 23 | 24 | beforeEach(resetHash) 25 | 26 | let node 27 | beforeEach(function () { 28 | node = document.createElement('div') 29 | }) 30 | 31 | afterEach(function () { 32 | unmountComponentAtNode(node) 33 | }) 34 | 35 | describe('when the target path contains a colon', function () { 36 | it('works', function (done) { 37 | const history = createHistory('/') 38 | const steps = [ 39 | function () { 40 | expect(this.state.location.pathname).toEqual('/') 41 | history.push('/home/hi:there') 42 | }, 43 | function () { 44 | expect(this.state.location.pathname).toEqual('/home/hi:there') 45 | } 46 | ] 47 | 48 | const execNextStep = execSteps(steps, done) 49 | 50 | render(( 51 | 52 | 53 | 54 | 55 | ), node, execNextStep) 56 | }) 57 | }) 58 | 59 | }) 60 | -------------------------------------------------------------------------------- /examples/huge-apps/components/GlobalNav.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | const dark = 'hsl(200, 20%, 20%)' 5 | const light = '#fff' 6 | const styles = {} 7 | 8 | styles.wrapper = { 9 | padding: '10px 20px', 10 | overflow: 'hidden', 11 | background: dark, 12 | color: light 13 | } 14 | 15 | styles.link = { 16 | padding: 11, 17 | color: light, 18 | fontWeight: 200 19 | } 20 | 21 | styles.activeLink = { 22 | ...styles.link, 23 | background: light, 24 | color: dark 25 | } 26 | 27 | class GlobalNav extends Component { 28 | 29 | constructor(props, context) { 30 | super(props, context) 31 | this.logOut = this.logOut.bind(this) 32 | } 33 | 34 | logOut() { 35 | alert('log out') 36 | } 37 | 38 | render() { 39 | const { user } = this.props 40 | 41 | return ( 42 |
43 |
44 | Home{' '} 45 | Calendar{' '} 46 | Grades{' '} 47 | Messages{' '} 48 |
49 |
50 | {user.name} 51 |
52 |
53 | ) 54 | } 55 | } 56 | 57 | GlobalNav.defaultProps = { 58 | user: { 59 | id: 1, 60 | name: 'Ryan Florence' 61 | } 62 | } 63 | 64 | export default GlobalNav 65 | -------------------------------------------------------------------------------- /examples/dynamic-segments/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link, Redirect } from 'react-router' 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 |
    10 |
  • Bob
  • 11 |
  • Sally
  • 12 |
13 | {this.props.children} 14 |
15 | ) 16 | } 17 | } 18 | 19 | class User extends Component { 20 | render() { 21 | const { userID } = this.props.params 22 | 23 | return ( 24 |
25 |

User id: {userID}

26 |
    27 |
  • foo task
  • 28 |
  • bar task
  • 29 |
30 | {this.props.children} 31 |
32 | ) 33 | } 34 | } 35 | 36 | class Task extends Component { 37 | render() { 38 | const { userID, taskID } = this.props.params 39 | 40 | return ( 41 |
42 |

User ID: {userID}

43 |

Task ID: {taskID}

44 |
45 | ) 46 | } 47 | } 48 | 49 | render(( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ), document.getElementById('example')) 59 | -------------------------------------------------------------------------------- /modules/deprecateObjectProperties.js: -------------------------------------------------------------------------------- 1 | import warning from './routerWarning' 2 | 3 | export let canUseMembrane = false 4 | 5 | // No-op by default. 6 | let deprecateObjectProperties = object => object 7 | 8 | if (__DEV__) { 9 | try { 10 | if (Object.defineProperty({}, 'x', { get() { return true } }).x) { 11 | canUseMembrane = true 12 | } 13 | /* eslint-disable no-empty */ 14 | } catch(e) {} 15 | /* eslint-enable no-empty */ 16 | 17 | if (canUseMembrane) { 18 | deprecateObjectProperties = (object, message) => { 19 | // Wrap the deprecated object in a membrane to warn on property access. 20 | const membrane = {} 21 | 22 | for (const prop in object) { 23 | if (!Object.prototype.hasOwnProperty.call(object, prop)) { 24 | continue 25 | } 26 | 27 | if (typeof object[prop] === 'function') { 28 | // Can't use fat arrow here because of use of arguments below. 29 | membrane[prop] = function () { 30 | warning(false, message) 31 | return object[prop].apply(object, arguments) 32 | } 33 | continue 34 | } 35 | 36 | // These properties are non-enumerable to prevent React dev tools from 37 | // seeing them and causing spurious warnings when accessing them. In 38 | // principle this could be done with a proxy, but support for the 39 | // ownKeys trap on proxies is not universal, even among browsers that 40 | // otherwise support proxies. 41 | Object.defineProperty(membrane, prop, { 42 | get() { 43 | warning(false, message) 44 | return object[prop] 45 | } 46 | }) 47 | } 48 | 49 | return membrane 50 | } 51 | } 52 | } 53 | 54 | export default deprecateObjectProperties 55 | -------------------------------------------------------------------------------- /modules/__tests__/Redirect-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import { render, unmountComponentAtNode } from 'react-dom' 4 | import createHistory from '../createMemoryHistory' 5 | import Redirect from '../Redirect' 6 | import Router from '../Router' 7 | import Route from '../Route' 8 | 9 | describe('A ', function () { 10 | 11 | let node 12 | beforeEach(function () { 13 | node = document.createElement('div') 14 | }) 15 | 16 | afterEach(function () { 17 | unmountComponentAtNode(node) 18 | }) 19 | 20 | it('works', function (done) { 21 | render(( 22 | 23 | 24 | 25 | 26 | ), node, function () { 27 | expect(this.state.location.pathname).toEqual('/messages/5') 28 | done() 29 | }) 30 | }) 31 | 32 | it('works with relative paths', function (done) { 33 | render(( 34 | 35 | 36 | 37 | 38 | 39 | 40 | ), node, function () { 41 | expect(this.state.location.pathname).toEqual('/nested/route2') 42 | done() 43 | }) 44 | }) 45 | 46 | it('works with relative paths with param', function (done) { 47 | render(( 48 | 49 | 50 | 51 | 52 | 53 | 54 | ), node, function () { 55 | expect(this.state.location.pathname).toEqual('/nested/1/route2') 56 | done() 57 | }) 58 | }) 59 | 60 | }) 61 | -------------------------------------------------------------------------------- /modules/AsyncUtils.js: -------------------------------------------------------------------------------- 1 | export function loopAsync(turns, work, callback) { 2 | let currentTurn = 0, isDone = false 3 | let sync = false, hasNext = false, doneArgs 4 | 5 | function done() { 6 | isDone = true 7 | if (sync) { 8 | // Iterate instead of recursing if possible. 9 | doneArgs = [ ...arguments ] 10 | return 11 | } 12 | 13 | callback.apply(this, arguments) 14 | } 15 | 16 | function next() { 17 | if (isDone) { 18 | return 19 | } 20 | 21 | hasNext = true 22 | if (sync) { 23 | // Iterate instead of recursing if possible. 24 | return 25 | } 26 | 27 | sync = true 28 | 29 | while (!isDone && currentTurn < turns && hasNext) { 30 | hasNext = false 31 | work.call(this, currentTurn++, next, done) 32 | } 33 | 34 | sync = false 35 | 36 | if (isDone) { 37 | // This means the loop finished synchronously. 38 | callback.apply(this, doneArgs) 39 | return 40 | } 41 | 42 | if (currentTurn >= turns && hasNext) { 43 | isDone = true 44 | callback() 45 | } 46 | } 47 | 48 | next() 49 | } 50 | 51 | export function mapAsync(array, work, callback) { 52 | const length = array.length 53 | const values = [] 54 | 55 | if (length === 0) 56 | return callback(null, values) 57 | 58 | let isDone = false, doneCount = 0 59 | 60 | function done(index, error, value) { 61 | if (isDone) 62 | return 63 | 64 | if (error) { 65 | isDone = true 66 | callback(error) 67 | } else { 68 | values[index] = value 69 | 70 | isDone = (++doneCount === length) 71 | 72 | if (isDone) 73 | callback(null, values) 74 | } 75 | } 76 | 77 | array.forEach(function (item, index) { 78 | work(item, index, function (error, value) { 79 | done(index, error, value) 80 | }) 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /examples/breadcrumbs/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link } from 'react-router' 4 | import './app.css' 5 | 6 | class App extends Component { 7 | render() { 8 | const depth = this.props.routes.length 9 | 10 | return ( 11 |
12 | 18 |
19 |
    20 | {this.props.routes.map((item, index) => 21 |
  • 22 | 26 | {item.component.title} 27 | 28 | {(index + 1) < depth && '\u2192'} 29 |
  • 30 | )} 31 |
32 | {this.props.children} 33 |
34 |
35 | ) 36 | } 37 | } 38 | 39 | App.title = 'Home' 40 | App.path = '/' 41 | 42 | 43 | class Products extends React.Component { 44 | render() { 45 | return ( 46 |
47 |

Products

48 |
49 | ) 50 | } 51 | } 52 | 53 | Products.title = 'Products' 54 | Products.path = '/products' 55 | 56 | class Orders extends React.Component { 57 | render() { 58 | return ( 59 |
60 |

Orders

61 |
62 | ) 63 | } 64 | } 65 | 66 | Orders.title = 'Orders' 67 | Orders.path = '/orders' 68 | 69 | render(( 70 | 71 | 72 | 73 | 74 | 75 | 76 | ), document.getElementById('example')) 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for contributing, you rock! 2 | 3 | If you use our code, it is now *our* code. 4 | 5 | Please read https://reactjs.org/ and the Code of Conduct before opening an issue. 6 | 7 | - [Think You Found a Bug?](#bug) 8 | - [Proposing New or Changed API?](#api) 9 | - [Issue Not Getting Attention?](#attention) 10 | - [Making a Pull Request?](#pr) 11 | - [Development](#development) 12 | - [Hacking](#hacking) 13 | 14 | 15 | ## Think You Found a Bug? 16 | 17 | Please provide a test case of some sort. Best is a pull request with a failing test. Next is a link to CodePen/JS Bin or repository that illustrates the bug. Finally, some copy/pastable code is acceptable. 18 | 19 | 20 | ## Proposing New or Changed API? 21 | 22 | Please provide thoughtful comments and some sample code. Proposals without substance will be closed. 23 | 24 | 25 | ## Issue Not Getting Attention? 26 | 27 | If you need a bug fixed and nobody is fixing it, it is your responsibility to fix it. Issues with no activity for 30 days may be closed. 28 | 29 | 30 | ## Making a Pull Request? 31 | 32 | Pull requests need only the :+1: of two or more collaborators to be merged; when the PR author is a collaborator, that counts as one. 33 | 34 | ### Tests 35 | 36 | All commits that fix bugs or add features need a test. 37 | 38 | ``Do not merge code without tests.`` 39 | 40 | ### Changelog 41 | 42 | All commits that change or add to the API must be done in a pull request that also: 43 | 44 | - Adds an entry to `CHANGES.md` with clear steps for updating code for changed or removed API 45 | - Updates examples 46 | - Updates the docs 47 | 48 | ## Development 49 | 50 | - `npm test` starts a karma test runner and watch for changes 51 | - `npm start` starts a webpack dev server that will watch for changes and build the examples 52 | 53 | ## Hacking 54 | 55 | The best way to hack on the router is to symlink it into your project using [`npm link`](https://docs.npmjs.com/cli/link). Then, use `npm run watch` to automatically watch the `modules` directory and output a new build every time something changes. 56 | -------------------------------------------------------------------------------- /modules/getComponents.js: -------------------------------------------------------------------------------- 1 | import { mapAsync } from './AsyncUtils' 2 | import { canUseMembrane } from './deprecateObjectProperties' 3 | import warning from './routerWarning' 4 | 5 | function getComponentsForRoute(nextState, route, callback) { 6 | if (route.component || route.components) { 7 | callback(null, route.component || route.components) 8 | return 9 | } 10 | 11 | const getComponent = route.getComponent || route.getComponents 12 | if (!getComponent) { 13 | callback() 14 | return 15 | } 16 | 17 | const { location } = nextState 18 | let nextStateWithLocation 19 | 20 | if (__DEV__ && canUseMembrane) { 21 | nextStateWithLocation = { ...nextState } 22 | 23 | // I don't use deprecateObjectProperties here because I want to keep the 24 | // same code path between development and production, in that we just 25 | // assign extra properties to the copy of the state object in both cases. 26 | for (const prop in location) { 27 | if (!Object.prototype.hasOwnProperty.call(location, prop)) { 28 | continue 29 | } 30 | 31 | Object.defineProperty(nextStateWithLocation, prop, { 32 | get() { 33 | warning(false, 'Accessing location properties from the first argument to `getComponent` and `getComponents` is deprecated. That argument is now the router state (`nextState`) rather than the location. To access the location, use `nextState.location`.') 34 | return location[prop] 35 | } 36 | }) 37 | } 38 | } else { 39 | nextStateWithLocation = { ...nextState, ...location } 40 | } 41 | 42 | getComponent.call(route, nextStateWithLocation, callback) 43 | } 44 | 45 | /** 46 | * Asynchronously fetches all components needed for the given router 47 | * state and calls callback(error, components) when finished. 48 | * 49 | * Note: This operation may finish synchronously if no routes have an 50 | * asynchronous getComponents method. 51 | */ 52 | function getComponents(nextState, callback) { 53 | mapAsync(nextState.routes, function (route, index, callback) { 54 | getComponentsForRoute(nextState, route, callback) 55 | }, callback) 56 | } 57 | 58 | export default getComponents 59 | -------------------------------------------------------------------------------- /modules/__tests__/useRouterHistory-test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import expect from 'expect' 3 | import React from 'react' 4 | import { render, unmountComponentAtNode } from 'react-dom' 5 | import useRouterHistory from '../useRouterHistory' 6 | import createHistory from 'history/lib/createMemoryHistory' 7 | import Redirect from '../Redirect' 8 | import Router from '../Router' 9 | import Route from '../Route' 10 | 11 | describe('useRouterHistory', function () { 12 | it('adds backwards compatibility flag', function () { 13 | const history = useRouterHistory(createHistory)() 14 | expect(history.__v2_compatible__).toBe(true) 15 | }) 16 | 17 | it('passes along options, especially query parsing', function (done) { 18 | const history = useRouterHistory(createHistory)({ 19 | stringifyQuery() { 20 | assert(true) 21 | done() 22 | } 23 | }) 24 | 25 | history.push({ pathname: '/', query: { test: true } }) 26 | }) 27 | 28 | describe('when using basename', function () { 29 | 30 | let node 31 | beforeEach(function () { 32 | node = document.createElement('div') 33 | }) 34 | 35 | afterEach(function () { 36 | unmountComponentAtNode(node) 37 | }) 38 | 39 | it('should regard basename', function (done) { 40 | const pathnames = [] 41 | const basenames = [] 42 | const history = useRouterHistory(createHistory)({ 43 | entries: '/foo/notes/5', 44 | basename: '/foo' 45 | }) 46 | history.listen(function (location) { 47 | pathnames.push(location.pathname) 48 | basenames.push(location.basename) 49 | }) 50 | render(( 51 | 52 | 53 | 54 | 55 | ), node, function () { 56 | expect(pathnames).toEqual([ '/notes/5', '/messages/5' ]) 57 | expect(basenames).toEqual([ '/foo', '/foo' ]) 58 | expect(this.state.location.pathname).toEqual('/messages/5') 59 | expect(this.state.location.basename).toEqual('/foo') 60 | done() 61 | }) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /modules/match.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant' 2 | 3 | import createMemoryHistory from './createMemoryHistory' 4 | import createTransitionManager from './createTransitionManager' 5 | import { createRoutes } from './RouteUtils' 6 | import { createRouterObject, createRoutingHistory } from './RouterUtils' 7 | 8 | /** 9 | * A high-level API to be used for server-side rendering. 10 | * 11 | * This function matches a location to a set of routes and calls 12 | * callback(error, redirectLocation, renderProps) when finished. 13 | * 14 | * Note: You probably don't want to use this in a browser unless you're using 15 | * server-side rendering with async routes. 16 | */ 17 | function match({ history, routes, location, ...options }, callback) { 18 | invariant( 19 | history || location, 20 | 'match needs a history or a location' 21 | ) 22 | 23 | history = history ? history : createMemoryHistory(options) 24 | const transitionManager = createTransitionManager( 25 | history, 26 | createRoutes(routes) 27 | ) 28 | 29 | let unlisten 30 | 31 | if (location) { 32 | // Allow match({ location: '/the/path', ... }) 33 | location = history.createLocation(location) 34 | } else { 35 | // Pick up the location from the history via synchronous history.listen 36 | // call if needed. 37 | unlisten = history.listen(historyLocation => { 38 | location = historyLocation 39 | }) 40 | } 41 | 42 | const router = createRouterObject(history, transitionManager) 43 | history = createRoutingHistory(history, transitionManager) 44 | 45 | transitionManager.match(location, function (error, redirectLocation, nextState) { 46 | callback( 47 | error, 48 | redirectLocation, 49 | nextState && { 50 | ...nextState, 51 | history, 52 | router, 53 | matchContext: { history, transitionManager, router } 54 | } 55 | ) 56 | 57 | // Defer removing the listener to here to prevent DOM histories from having 58 | // to unwind DOM event listeners unnecessarily, in case callback renders a 59 | // and attaches another history listener. 60 | if (unlisten) { 61 | unlisten() 62 | } 63 | }) 64 | } 65 | 66 | export default match 67 | -------------------------------------------------------------------------------- /examples/confirming-navigation/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link, withRouter } from 'react-router' 4 | 5 | const App = React.createClass({ 6 | render() { 7 | return ( 8 |
9 |
    10 |
  • Dashboard
  • 11 |
  • Form
  • 12 |
13 | {this.props.children} 14 |
15 | ) 16 | } 17 | }) 18 | 19 | const Dashboard = React.createClass({ 20 | render() { 21 | return

Dashboard

22 | } 23 | }) 24 | 25 | const Form = withRouter( 26 | React.createClass({ 27 | 28 | componentWillMount() { 29 | this.props.router.setRouteLeaveHook( 30 | this.props.route, 31 | this.routerWillLeave 32 | ) 33 | }, 34 | 35 | getInitialState() { 36 | return { 37 | textValue: 'ohai' 38 | } 39 | }, 40 | 41 | routerWillLeave() { 42 | if (this.state.textValue) 43 | return 'You have unsaved information, are you sure you want to leave this page?' 44 | }, 45 | 46 | handleChange(event) { 47 | this.setState({ 48 | textValue: event.target.value 49 | }) 50 | }, 51 | 52 | handleSubmit(event) { 53 | event.preventDefault() 54 | 55 | this.setState({ 56 | textValue: '' 57 | }, () => { 58 | this.props.router.push('/') 59 | }) 60 | }, 61 | 62 | render() { 63 | return ( 64 |
65 |
66 |

Click the dashboard link with text in the input.

67 | 68 | 69 |
70 |
71 | ) 72 | } 73 | }) 74 | ) 75 | 76 | render(( 77 | 78 | 79 | 80 | 81 | 82 | 83 | ), document.getElementById('example')) 84 | -------------------------------------------------------------------------------- /examples/passing-props-to-children/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link, withRouter } from 'react-router' 4 | import './app.css' 5 | 6 | const App = withRouter( 7 | React.createClass({ 8 | 9 | getInitialState() { 10 | return { 11 | tacos: [ 12 | { name: 'duck confit' }, 13 | { name: 'carne asada' }, 14 | { name: 'shrimp' } 15 | ] 16 | } 17 | }, 18 | 19 | addTaco() { 20 | let name = prompt('taco name?') 21 | 22 | this.setState({ 23 | tacos: this.state.tacos.concat({ name }) 24 | }) 25 | }, 26 | 27 | handleRemoveTaco(removedTaco) { 28 | this.setState({ 29 | tacos: this.state.tacos.filter(function (taco) { 30 | return taco.name != removedTaco 31 | }) 32 | }) 33 | 34 | this.props.router.push('/') 35 | }, 36 | 37 | render() { 38 | let links = this.state.tacos.map(function (taco, i) { 39 | return ( 40 |
  • 41 | {taco.name} 42 |
  • 43 | ) 44 | }) 45 | return ( 46 |
    47 | 48 |
      49 | {links} 50 |
    51 |
    52 | {this.props.children && React.cloneElement(this.props.children, { 53 | onRemoveTaco: this.handleRemoveTaco 54 | })} 55 |
    56 |
    57 | ) 58 | } 59 | }) 60 | ) 61 | 62 | const Taco = React.createClass({ 63 | remove() { 64 | this.props.onRemoveTaco(this.props.params.name) 65 | }, 66 | 67 | render() { 68 | return ( 69 |
    70 |

    {this.props.params.name}

    71 | 72 |
    73 | ) 74 | } 75 | }) 76 | 77 | render(( 78 | 79 | 80 | 81 | 82 | 83 | ), document.getElementById('example')) 84 | -------------------------------------------------------------------------------- /modules/Lifecycle.js: -------------------------------------------------------------------------------- 1 | import warning from './routerWarning' 2 | import React from 'react' 3 | import invariant from 'invariant' 4 | 5 | const { object } = React.PropTypes 6 | 7 | /** 8 | * The Lifecycle mixin adds the routerWillLeave lifecycle method to a 9 | * component that may be used to cancel a transition or prompt the user 10 | * for confirmation. 11 | * 12 | * On standard transitions, routerWillLeave receives a single argument: the 13 | * location we're transitioning to. To cancel the transition, return false. 14 | * To prompt the user for confirmation, return a prompt message (string). 15 | * 16 | * During the beforeunload event (assuming you're using the useBeforeUnload 17 | * history enhancer), routerWillLeave does not receive a location object 18 | * because it isn't possible for us to know the location we're transitioning 19 | * to. In this case routerWillLeave must return a prompt message to prevent 20 | * the user from closing the window/tab. 21 | */ 22 | const Lifecycle = { 23 | 24 | contextTypes: { 25 | history: object.isRequired, 26 | // Nested children receive the route as context, either 27 | // set by the route component using the RouteContext mixin 28 | // or by some other ancestor. 29 | route: object 30 | }, 31 | 32 | propTypes: { 33 | // Route components receive the route object as a prop. 34 | route: object 35 | }, 36 | 37 | componentDidMount() { 38 | warning(false, 'the `Lifecycle` mixin is deprecated, please use `context.router.setRouteLeaveHook(route, hook)`. http://tiny.cc/router-lifecyclemixin') 39 | invariant( 40 | this.routerWillLeave, 41 | 'The Lifecycle mixin requires you to define a routerWillLeave method' 42 | ) 43 | 44 | const route = this.props.route || this.context.route 45 | 46 | invariant( 47 | route, 48 | 'The Lifecycle mixin must be used on either a) a or ' + 49 | 'b) a descendant of a that uses the RouteContext mixin' 50 | ) 51 | 52 | this._unlistenBeforeLeavingRoute = this.context.history.listenBeforeLeavingRoute( 53 | route, 54 | this.routerWillLeave 55 | ) 56 | }, 57 | 58 | componentWillUnmount() { 59 | if (this._unlistenBeforeLeavingRoute) 60 | this._unlistenBeforeLeavingRoute() 61 | } 62 | 63 | } 64 | 65 | export default Lifecycle 66 | -------------------------------------------------------------------------------- /docs/guides/RouteMatching.md: -------------------------------------------------------------------------------- 1 | # Route Matching 2 | 3 | A [route](/docs/Glossary.md#route) has three attributes that determine whether or not it "matches" the URL: 4 | 5 | 1. [nesting](#nesting) and 6 | 2. its [`path`](#path-syntax) 7 | 3. its [precedence](#precedence) 8 | 9 | ### Nesting 10 | React Router uses the concept of nested routes to let you declare nested sets of views that should be rendered when a given URL is invoked. Nested routes are arranged in a tree-like structure. To find a match, React Router traverses the [route config](/docs/Glossary.md#routeconfig) depth-first searching for a route that matches the URL. 11 | 12 | ### Path Syntax 13 | A route path is [a string pattern](/docs/Glossary.md#routepattern) that is used to match a URL (or a portion of one). Route paths are interpreted literally, except for the following special symbols: 14 | 15 | - `:paramName` – matches a URL segment up to the next `/`, `?`, or `#`. The matched string is called a [param](/docs/Glossary.md#params) 16 | - `()` – Wraps a portion of the URL that is optional 17 | - `*` – Matches all characters (non-greedy) up to the next character in the pattern, or to the end of the URL if there is none, and creates a `splat` [param](/docs/Glossary.md#params) 18 | - `**` - Matches all characters (greedy) until the next `/`, `?`, or `#` and creates a `splat` [param](/docs/Glossary.md#params) 19 | 20 | ```js 21 | // matches /hello/michael and /hello/ryan 22 | // matches /hello, /hello/michael, and /hello/ryan 23 | // matches /files/hello.jpg and /files/hello.html 24 | // matches /files/hello.jpg and /files/path/to/file.jpg 25 | ``` 26 | 27 | If a route uses a relative `path`, it builds upon the accumulated `path` of its ancestors. Nested routes may opt-out of this behavior by [using an absolute `path`](RouteConfiguration.md#decoupling-the-ui-from-the-url). 28 | 29 | ### Precedence 30 | Finally, the routing algorithm attempts to match routes in the order they are defined, top to bottom. So, when you have two sibling routes you should be sure the first doesn't match all possible `path`s that can be matched by the later sibling. For example, **don't** do this: 31 | 32 | ```js 33 | 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /modules/computeChangedRoutes.js: -------------------------------------------------------------------------------- 1 | import { getParamNames } from './PatternUtils' 2 | 3 | function routeParamsChanged(route, prevState, nextState) { 4 | if (!route.path) 5 | return false 6 | 7 | const paramNames = getParamNames(route.path) 8 | 9 | return paramNames.some(function (paramName) { 10 | return prevState.params[paramName] !== nextState.params[paramName] 11 | }) 12 | } 13 | 14 | /** 15 | * Returns an object of { leaveRoutes, changeRoutes, enterRoutes } determined by 16 | * the change from prevState to nextState. We leave routes if either 17 | * 1) they are not in the next state or 2) they are in the next state 18 | * but their params have changed (i.e. /users/123 => /users/456). 19 | * 20 | * leaveRoutes are ordered starting at the leaf route of the tree 21 | * we're leaving up to the common parent route. enterRoutes are ordered 22 | * from the top of the tree we're entering down to the leaf route. 23 | * 24 | * changeRoutes are any routes that didn't leave or enter during 25 | * the transition. 26 | */ 27 | function computeChangedRoutes(prevState, nextState) { 28 | const prevRoutes = prevState && prevState.routes 29 | const nextRoutes = nextState.routes 30 | 31 | let leaveRoutes, changeRoutes, enterRoutes 32 | if (prevRoutes) { 33 | let parentIsLeaving = false 34 | leaveRoutes = prevRoutes.filter(function (route) { 35 | if (parentIsLeaving) { 36 | return true 37 | } else { 38 | const isLeaving = nextRoutes.indexOf(route) === -1 || routeParamsChanged(route, prevState, nextState) 39 | if (isLeaving) 40 | parentIsLeaving = true 41 | return isLeaving 42 | } 43 | }) 44 | 45 | // onLeave hooks start at the leaf route. 46 | leaveRoutes.reverse() 47 | 48 | enterRoutes = [] 49 | changeRoutes = [] 50 | 51 | nextRoutes.forEach(function (route) { 52 | const isNew = prevRoutes.indexOf(route) === -1 53 | const paramsChanged = leaveRoutes.indexOf(route) !== -1 54 | 55 | if (isNew || paramsChanged) 56 | enterRoutes.push(route) 57 | else 58 | changeRoutes.push(route) 59 | }) 60 | 61 | } else { 62 | leaveRoutes = [] 63 | changeRoutes = [] 64 | enterRoutes = nextRoutes 65 | } 66 | 67 | return { 68 | leaveRoutes, 69 | changeRoutes, 70 | enterRoutes 71 | } 72 | } 73 | 74 | export default computeChangedRoutes 75 | -------------------------------------------------------------------------------- /examples/sidebar/data.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | name: 'Tacos', 4 | description: 'A taco (/ˈtækoʊ/ or /ˈtɑːkoʊ/) is a traditional Mexican dish composed of a corn or wheat tortilla folded or rolled around a filling. A taco can be made with a variety of fillings, including beef, pork, chicken, seafood, vegetables and cheese, allowing for great versatility and variety. A taco is generally eaten without utensils and is often accompanied by garnishes such as salsa, avocado or guacamole, cilantro (coriander), tomatoes, minced meat, onions and lettuce.', 5 | items: [ 6 | { name: 'Carne Asada', price: 7 }, 7 | { name: 'Pollo', price: 6 }, 8 | { name: 'Carnitas', price: 6 } 9 | ] 10 | }, 11 | { 12 | name: 'Burgers', 13 | description: 'A hamburger (also called a beef burger, hamburger sandwich, burger or hamburg) is a sandwich consisting of one or more cooked patties of ground meat, usually beef, placed inside a sliced bun. Hamburgers are often served with lettuce, bacon, tomato, onion, pickles, cheese and condiments such as mustard, mayonnaise, ketchup, relish, and green chile.', 14 | items: [ 15 | { name: 'Buffalo Bleu', price: 8 }, 16 | { name: 'Bacon', price: 8 }, 17 | { name: 'Mushroom and Swiss', price: 6 } 18 | ] 19 | }, 20 | { 21 | name: 'Drinks', 22 | description: 'Drinks, or beverages, are liquids intended for human consumption. In addition to basic needs, beverages form part of the culture of human society. Although all beverages, including juice, soft drinks, and carbonated drinks, have some form of water in them, water itself is often not classified as a beverage, and the word beverage has been recurrently defined as not referring to water.', 23 | items: [ 24 | { name: 'Lemonade', price: 3 }, 25 | { name: 'Root Beer', price: 4 }, 26 | { name: 'Iron Port', price: 5 } 27 | ] 28 | } 29 | ] 30 | 31 | const dataMap = data.reduce(function (map, category) { 32 | category.itemsMap = category.items.reduce(function (itemsMap, item) { 33 | itemsMap[item.name] = item 34 | return itemsMap 35 | }, {}) 36 | map[category.name] = category 37 | return map 38 | }, {}) 39 | 40 | exports.getAll = function () { 41 | return data 42 | } 43 | 44 | exports.lookupCategory = function (name) { 45 | return dataMap[name] 46 | } 47 | 48 | exports.lookupItem = function (category, item) { 49 | return dataMap[category].itemsMap[item] 50 | } 51 | -------------------------------------------------------------------------------- /examples/active-links/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | import { Router, Route, IndexRoute, Link, IndexLink, browserHistory } from 'react-router' 4 | 5 | const ACTIVE = { color: 'red' } 6 | 7 | class App extends Component { 8 | render() { 9 | return ( 10 |
    11 |

    APP!

    12 |
      13 |
    • /
    • 14 |
    • / IndexLink
    • 15 | 16 |
    • /users
    • 17 |
    • /users IndexLink
    • 18 | 19 |
    • /users/ryan
    • 20 |
    • /users/ryan?foo=bar
    • 22 | 23 |
    • /about
    • 24 |
    25 | 26 | {this.props.children} 27 |
    28 | ) 29 | } 30 | } 31 | 32 | class Index extends React.Component { 33 | render() { 34 | return ( 35 |
    36 |

    Index!

    37 |
    38 | ) 39 | } 40 | } 41 | 42 | class Users extends React.Component { 43 | render() { 44 | return ( 45 |
    46 |

    Users

    47 | {this.props.children} 48 |
    49 | ) 50 | } 51 | } 52 | 53 | class UsersIndex extends React.Component { 54 | render() { 55 | return ( 56 |
    57 |

    UsersIndex

    58 |
    59 | ) 60 | } 61 | } 62 | 63 | class User extends React.Component { 64 | render() { 65 | return ( 66 |
    67 |

    User {this.props.params.id}

    68 |
    69 | ) 70 | } 71 | } 72 | 73 | class About extends React.Component { 74 | render() { 75 | return ( 76 |
    77 |

    About

    78 |
    79 | ) 80 | } 81 | } 82 | 83 | render(( 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | ), document.getElementById('example')) 95 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at rpflorence@gmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | 52 | -------------------------------------------------------------------------------- /modules/Redirect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import invariant from 'invariant' 3 | import { createRouteFromReactElement } from './RouteUtils' 4 | import { formatPattern } from './PatternUtils' 5 | import { falsy } from './InternalPropTypes' 6 | 7 | const { string, object } = React.PropTypes 8 | 9 | /** 10 | * A is used to declare another URL path a client should 11 | * be sent to when they request a given URL. 12 | * 13 | * Redirects are placed alongside routes in the route configuration 14 | * and are traversed in the same manner. 15 | */ 16 | const Redirect = React.createClass({ 17 | 18 | statics: { 19 | 20 | createRouteFromReactElement(element) { 21 | const route = createRouteFromReactElement(element) 22 | 23 | if (route.from) 24 | route.path = route.from 25 | 26 | route.onEnter = function (nextState, replace) { 27 | const { location, params } = nextState 28 | 29 | let pathname 30 | if (route.to.charAt(0) === '/') { 31 | pathname = formatPattern(route.to, params) 32 | } else if (!route.to) { 33 | pathname = location.pathname 34 | } else { 35 | let routeIndex = nextState.routes.indexOf(route) 36 | let parentPattern = Redirect.getRoutePattern(nextState.routes, routeIndex - 1) 37 | let pattern = parentPattern.replace(/\/*$/, '/') + route.to 38 | pathname = formatPattern(pattern, params) 39 | } 40 | 41 | replace({ 42 | pathname, 43 | query: route.query || location.query, 44 | state: route.state || location.state 45 | }) 46 | } 47 | 48 | return route 49 | }, 50 | 51 | getRoutePattern(routes, routeIndex) { 52 | let parentPattern = '' 53 | 54 | for (let i = routeIndex; i >= 0; i--) { 55 | const route = routes[i] 56 | const pattern = route.path || '' 57 | 58 | parentPattern = pattern.replace(/\/*$/, '/') + parentPattern 59 | 60 | if (pattern.indexOf('/') === 0) 61 | break 62 | } 63 | 64 | return '/' + parentPattern 65 | } 66 | 67 | }, 68 | 69 | propTypes: { 70 | path: string, 71 | from: string, // Alias for path 72 | to: string.isRequired, 73 | query: object, 74 | state: object, 75 | onEnter: falsy, 76 | children: falsy 77 | }, 78 | 79 | /* istanbul ignore next: sanity check */ 80 | render() { 81 | invariant( 82 | false, 83 | ' elements are for router configuration only and should not be rendered' 84 | ) 85 | } 86 | 87 | }) 88 | 89 | export default Redirect 90 | -------------------------------------------------------------------------------- /examples/auth-flow-async-with-query-params/app.js: -------------------------------------------------------------------------------- 1 | import React, { createClass } from 'react' 2 | import { render } from 'react-dom' 3 | import { 4 | Router, Route, IndexRoute, browserHistory, Link, withRouter 5 | } from 'react-router' 6 | 7 | function App(props) { 8 | return ( 9 |
    10 | {props.children} 11 |
    12 | ) 13 | } 14 | 15 | const Form = withRouter( 16 | createClass({ 17 | 18 | getInitialState() { 19 | return { 20 | value: '' 21 | } 22 | }, 23 | 24 | submitAction(event) { 25 | event.preventDefault() 26 | this.props.router.push({ 27 | pathname: '/page', 28 | query: { 29 | qsparam: this.state.value 30 | } 31 | }) 32 | }, 33 | 34 | handleChange(event) { 35 | this.setState({ value: event.target.value }) 36 | }, 37 | 38 | render() { 39 | return ( 40 |
    41 |

    Token is pancakes

    42 | 43 | 44 |

    Or authenticate via URL

    45 |

    Or try failing to authenticate via URL

    46 |
    47 | ) 48 | } 49 | }) 50 | ) 51 | 52 | function Page() { 53 | return

    Hey, I see you are authenticated. Welcome!

    54 | } 55 | 56 | function ErrorPage() { 57 | return

    Oh no! Your auth failed!

    58 | } 59 | 60 | function requireCredentials(nextState, replace, next) { 61 | const query = nextState.location.query 62 | if (query.qsparam) { 63 | serverAuth(query.qsparam) 64 | .then( 65 | () => next(), 66 | () => { 67 | replace('/error') 68 | next() 69 | } 70 | ) 71 | } else { 72 | replace('/error') 73 | next() 74 | } 75 | } 76 | 77 | function serverAuth(authToken) { 78 | return new Promise((resolve, reject) => { 79 | // That server is gonna take a while 80 | setTimeout(() => { 81 | if(authToken === 'pancakes') { 82 | resolve('authenticated') 83 | } else { 84 | reject('nope') 85 | } 86 | }, 200) 87 | }) 88 | } 89 | 90 | render(( 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | ), document.getElementById('example')) 99 | -------------------------------------------------------------------------------- /docs/guides/IndexRoutes.md: -------------------------------------------------------------------------------- 1 | # Index Routes and Index Links 2 | 3 | ## Index Routes 4 | 5 | To illustrate the use case for `IndexRoute`, imagine the following route 6 | config without it: 7 | 8 | ```js 9 | 10 | 11 | 12 | 13 | 14 | 15 | ``` 16 | 17 | When the user visits `/`, the App component is rendered, but none of the 18 | children are, so `this.props.children` inside of `App` will be undefined. 19 | To render some default UI you could easily do `{this.props.children || 20 | }`. 21 | 22 | But now `Home` can't participate in routing, like the `onEnter` hooks, 23 | etc. You render in the same position as `Accounts` and `Statements`, so 24 | the router allows you to have `Home` be a first class route component with 25 | `IndexRoute`. 26 | 27 | ```js 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ``` 36 | 37 | Now `App` can render `{this.props.children}` and we have a first-class 38 | route for `Home` that can participate in routing. 39 | 40 | ## Index Redirects 41 | 42 | Suppose your basic route configuration looks like: 43 | 44 | ```js 45 | 46 | 47 | 48 | 49 | ``` 50 | 51 | Suppose you want to redirect `/` to `/welcome`. To do this, you need to set up 52 | an index route that does the redirect. To do this, use the `` 53 | component: 54 | 55 | ```js 56 | 57 | 58 | 59 | 60 | 61 | ``` 62 | 63 | This is equivalent to setting up an index route with just an `onEnter` hook 64 | that redirects the user. You would set this up with plain routes as: 65 | 66 | ```js 67 | const routes = [{ 68 | path: '/', 69 | component: App, 70 | indexRoute: { onEnter: (nextState, replace) => replace('/welcome') }, 71 | childRoutes: [ 72 | { path: 'welcome', component: Welcome }, 73 | { path: 'about', component: About } 74 | ] 75 | }] 76 | ``` 77 | 78 | ## Index Links 79 | 80 | If you were to `Home` in this app, it would always 81 | be active since every URL starts with `/`. This is a problem because 82 | we'd like to link to `Home` but only be active if `Home` is rendered. 83 | 84 | To have a link to `/` that is only active when the `Home` route is 85 | rendered, use `Home`. 86 | -------------------------------------------------------------------------------- /examples/animations/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group' 4 | import { browserHistory, Router, Route, IndexRoute, Link } from 'react-router' 5 | import './app.css' 6 | 7 | class App extends Component { 8 | render() { 9 | return ( 10 |
    11 |
      12 |
    • Page 1
    • 13 |
    • Page 2
    • 14 |
    15 | 16 | 22 | {React.cloneElement(this.props.children, { 23 | key: this.props.location.pathname 24 | })} 25 | 26 | 27 |
    28 | ) 29 | } 30 | } 31 | 32 | 33 | class Index extends Component { 34 | render() { 35 | return ( 36 |
    37 |

    Index

    38 |

    Animations with React Router are not different than any other animation.

    39 |
    40 | ) 41 | } 42 | } 43 | 44 | class Page1 extends Component { 45 | render() { 46 | return ( 47 |
    48 |

    Page 1

    49 |

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    50 |
    51 | ) 52 | } 53 | } 54 | 55 | class Page2 extends Component { 56 | render() { 57 | return ( 58 |
    59 |

    Page 2

    60 |

    Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    61 |
    62 | ) 63 | } 64 | } 65 | 66 | render(( 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ), document.getElementById('example')) 75 | -------------------------------------------------------------------------------- /modules/PropTypes.js: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'react' 2 | 3 | import deprecateObjectProperties from './deprecateObjectProperties' 4 | import * as InternalPropTypes from './InternalPropTypes' 5 | import warning from './routerWarning' 6 | 7 | const { func, object, shape, string } = PropTypes 8 | 9 | export const routerShape = shape({ 10 | push: func.isRequired, 11 | replace: func.isRequired, 12 | go: func.isRequired, 13 | goBack: func.isRequired, 14 | goForward: func.isRequired, 15 | setRouteLeaveHook: func.isRequired, 16 | isActive: func.isRequired 17 | }) 18 | 19 | export const locationShape = shape({ 20 | pathname: string.isRequired, 21 | search: string.isRequired, 22 | state: object, 23 | action: string.isRequired, 24 | key: string 25 | }) 26 | 27 | // Deprecated stuff below: 28 | 29 | export let falsy = InternalPropTypes.falsy 30 | export let history = InternalPropTypes.history 31 | export let location = locationShape 32 | export let component = InternalPropTypes.component 33 | export let components = InternalPropTypes.components 34 | export let route = InternalPropTypes.route 35 | export let routes = InternalPropTypes.routes 36 | export let router = routerShape 37 | 38 | if (__DEV__) { 39 | const deprecatePropType = (propType, message) => (...args) => { 40 | warning(false, message) 41 | return propType(...args) 42 | } 43 | 44 | const deprecateInternalPropType = propType => ( 45 | deprecatePropType(propType, 'This prop type is not intended for external use, and was previously exported by mistake. These internal prop types are deprecated for external use, and will be removed in a later version.') 46 | ) 47 | 48 | const deprecateRenamedPropType = (propType, name) => ( 49 | deprecatePropType(propType, `The \`${name}\` prop type is now exported as \`${name}Shape\` to avoid name conflicts. This export is deprecated and will be removed in a later version.`) 50 | ) 51 | 52 | falsy = deprecateInternalPropType(falsy) 53 | history = deprecateInternalPropType(history) 54 | component = deprecateInternalPropType(component) 55 | components = deprecateInternalPropType(components) 56 | route = deprecateInternalPropType(route) 57 | routes = deprecateInternalPropType(routes) 58 | 59 | location = deprecateRenamedPropType(location, 'location') 60 | router = deprecateRenamedPropType(router, 'router') 61 | } 62 | 63 | let defaultExport = { 64 | falsy, 65 | history, 66 | location, 67 | component, 68 | components, 69 | route, 70 | // For some reason, routes was never here. 71 | router 72 | } 73 | 74 | if (__DEV__) { 75 | defaultExport = deprecateObjectProperties(defaultExport, 'The default export from `react-router/lib/PropTypes` is deprecated. Please use the named exports instead.') 76 | } 77 | 78 | export default defaultExport 79 | -------------------------------------------------------------------------------- /examples/master-detail/ContactStore.js: -------------------------------------------------------------------------------- 1 | const API = 'http://addressbook-api.herokuapp.com/contacts' 2 | 3 | let _contacts = {} 4 | let _initCalled = false 5 | let _changeListeners = [] 6 | 7 | const ContactStore = { 8 | 9 | init: function () { 10 | if (_initCalled) 11 | return 12 | 13 | _initCalled = true 14 | 15 | getJSON(API, function (err, res) { 16 | res.contacts.forEach(function (contact) { 17 | _contacts[contact.id] = contact 18 | }) 19 | 20 | ContactStore.notifyChange() 21 | }) 22 | }, 23 | 24 | addContact: function (contact, cb) { 25 | postJSON(API, { contact: contact }, function (res) { 26 | _contacts[res.contact.id] = res.contact 27 | ContactStore.notifyChange() 28 | if (cb) cb(res.contact) 29 | }) 30 | }, 31 | 32 | removeContact: function (id, cb) { 33 | deleteJSON(API + '/' + id, cb) 34 | delete _contacts[id] 35 | ContactStore.notifyChange() 36 | }, 37 | 38 | getContacts: function () { 39 | const array = [] 40 | 41 | for (const id in _contacts) 42 | array.push(_contacts[id]) 43 | 44 | return array 45 | }, 46 | 47 | getContact: function (id) { 48 | return _contacts[id] 49 | }, 50 | 51 | notifyChange: function () { 52 | _changeListeners.forEach(function (listener) { 53 | listener() 54 | }) 55 | }, 56 | 57 | addChangeListener: function (listener) { 58 | _changeListeners.push(listener) 59 | }, 60 | 61 | removeChangeListener: function (listener) { 62 | _changeListeners = _changeListeners.filter(function (l) { 63 | return listener !== l 64 | }) 65 | } 66 | 67 | } 68 | 69 | localStorage.token = localStorage.token || (Date.now()*Math.random()) 70 | 71 | function getJSON(url, cb) { 72 | const req = new XMLHttpRequest() 73 | req.onload = function () { 74 | if (req.status === 404) { 75 | cb(new Error('not found')) 76 | } else { 77 | cb(null, JSON.parse(req.response)) 78 | } 79 | } 80 | req.open('GET', url) 81 | req.setRequestHeader('authorization', localStorage.token) 82 | req.send() 83 | } 84 | 85 | function postJSON(url, obj, cb) { 86 | const req = new XMLHttpRequest() 87 | req.onload = function () { 88 | cb(JSON.parse(req.response)) 89 | } 90 | req.open('POST', url) 91 | req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') 92 | req.setRequestHeader('authorization', localStorage.token) 93 | req.send(JSON.stringify(obj)) 94 | } 95 | 96 | function deleteJSON(url, cb) { 97 | const req = new XMLHttpRequest() 98 | req.onload = cb 99 | req.open('DELETE', url) 100 | req.setRequestHeader('authorization', localStorage.token) 101 | req.send() 102 | } 103 | 104 | export default ContactStore 105 | -------------------------------------------------------------------------------- /modules/__tests__/createRoutesFromReactChildren-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React, { Component } from 'react' 3 | import { createRoutesFromReactChildren } from '../RouteUtils' 4 | import IndexRoute from '../IndexRoute' 5 | import Route from '../Route' 6 | 7 | describe('createRoutesFromReactChildren', function () { 8 | 9 | class Parent extends Component { 10 | render() { 11 | return ( 12 |
    13 |

    Parent

    14 | {this.props.children} 15 |
    16 | ) 17 | } 18 | } 19 | 20 | class Hello extends Component { 21 | render() { 22 | return
    Hello
    23 | } 24 | } 25 | 26 | class Goodbye extends Component { 27 | render() { 28 | return
    Goodbye
    29 | } 30 | } 31 | 32 | it('works with index routes', function () { 33 | const routes = createRoutesFromReactChildren( 34 | 35 | 36 | 37 | ) 38 | 39 | expect(routes).toEqual([ 40 | { 41 | path: '/', 42 | component: Parent, 43 | indexRoute: { 44 | component: Hello 45 | } 46 | } 47 | ]) 48 | }) 49 | 50 | it('works with nested routes', function () { 51 | const routes = createRoutesFromReactChildren( 52 | 53 | 54 | 55 | ) 56 | 57 | expect(routes).toEqual([ 58 | { 59 | component: Parent, 60 | childRoutes: [ 61 | { 62 | path: 'home', 63 | components: { hello: Hello, goodbye: Goodbye } 64 | } 65 | ] 66 | } 67 | ]) 68 | }) 69 | 70 | it('works with falsy children', function () { 71 | const routes = createRoutesFromReactChildren([ 72 | , 73 | null, 74 | , 75 | undefined 76 | ]) 77 | 78 | expect(routes).toEqual([ 79 | { 80 | path: '/one', 81 | component: Parent 82 | }, { 83 | path: '/two', 84 | component: Parent 85 | } 86 | ]) 87 | }) 88 | 89 | it('works with comments', function () { 90 | const routes = createRoutesFromReactChildren( 91 | 92 | // This is a comment. 93 | 94 | 95 | ) 96 | 97 | expect(routes).toEqual([ 98 | { 99 | path: '/one', 100 | component: Parent, 101 | childRoutes: [ 102 | { 103 | path: '/two', 104 | component: Hello 105 | } 106 | ] 107 | } 108 | ]) 109 | }) 110 | 111 | }) 112 | -------------------------------------------------------------------------------- /examples/huge-apps/app.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-unused-vars */ 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import { Router, browserHistory } from 'react-router' 5 | import stubbedCourses from './stubs/COURSES' 6 | 7 | const rootRoute = { 8 | component: 'div', 9 | childRoutes: [ { 10 | path: '/', 11 | component: require('./components/App'), 12 | childRoutes: [ 13 | require('./routes/Calendar'), 14 | require('./routes/Course'), 15 | require('./routes/Grades'), 16 | require('./routes/Messages'), 17 | require('./routes/Profile') 18 | ] 19 | } ] 20 | } 21 | 22 | render( 23 | , 24 | document.getElementById('example') 25 | ) 26 | 27 | // I've unrolled the recursive directory loop that is happening above to get a 28 | // better idea of just what this huge-apps Router looks like, or just look at the 29 | // file system :) 30 | // 31 | // import { Route } from 'react-router' 32 | 33 | // import App from './components/App' 34 | // import Course from './routes/Course/components/Course' 35 | // import AnnouncementsSidebar from './routes/Course/routes/Announcements/components/Sidebar' 36 | // import Announcements from './routes/Course/routes/Announcements/components/Announcements' 37 | // import Announcement from './routes/Course/routes/Announcements/routes/Announcement/components/Announcement' 38 | // import AssignmentsSidebar from './routes/Course/routes/Assignments/components/Sidebar' 39 | // import Assignments from './routes/Course/routes/Assignments/components/Assignments' 40 | // import Assignment from './routes/Course/routes/Assignments/routes/Assignment/components/Assignment' 41 | // import CourseGrades from './routes/Course/routes/Grades/components/Grades' 42 | // import Calendar from './routes/Calendar/components/Calendar' 43 | // import Grades from './routes/Grades/components/Grades' 44 | // import Messages from './routes/Messages/components/Messages' 45 | 46 | // render( 47 | // 48 | // 49 | // 50 | // 51 | // 55 | // 56 | // 57 | // 61 | // 62 | // 63 | // 64 | // 65 | // 66 | // 67 | // 68 | // 69 | // , 70 | // document.getElementById('example') 71 | // ) 72 | -------------------------------------------------------------------------------- /examples/sidebar/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link } from 'react-router' 4 | import data from './data' 5 | import './app.css' 6 | 7 | class Category extends Component { 8 | render() { 9 | const category = data.lookupCategory(this.props.params.category) 10 | 11 | return ( 12 |
    13 |

    {category.name}

    14 | {this.props.children || ( 15 |

    {category.description}

    16 | )} 17 |
    18 | ) 19 | } 20 | } 21 | 22 | class CategorySidebar extends Component { 23 | render() { 24 | const category = data.lookupCategory(this.props.params.category) 25 | 26 | return ( 27 |
    28 | ◀︎ Back 29 |

    {category.name} Items

    30 |
      31 | {category.items.map((item, index) => ( 32 |
    • 33 | {item.name} 34 |
    • 35 | ))} 36 |
    37 |
    38 | ) 39 | } 40 | } 41 | 42 | class Item extends Component { 43 | render() { 44 | const { category, item } = this.props.params 45 | const menuItem = data.lookupItem(category, item) 46 | 47 | return ( 48 |
    49 |

    {menuItem.name}

    50 |

    ${menuItem.price}

    51 |
    52 | ) 53 | } 54 | } 55 | 56 | class Index extends Component { 57 | render() { 58 | return ( 59 |
    60 |

    Sidebar

    61 |

    62 | Routes can have multiple components, so that all portions of your UI 63 | can participate in the routing. 64 |

    65 |
    66 | ) 67 | } 68 | } 69 | 70 | class IndexSidebar extends Component { 71 | render() { 72 | return ( 73 |
    74 |

    Categories

    75 |
      76 | {data.getAll().map((category, index) => ( 77 |
    • 78 | {category.name} 79 |
    • 80 | ))} 81 |
    82 |
    83 | ) 84 | } 85 | } 86 | 87 | class App extends Component { 88 | render() { 89 | const { content, sidebar } = this.props 90 | 91 | return ( 92 |
    93 |
    94 | {sidebar || } 95 |
    96 |
    97 | {content || } 98 |
    99 |
    100 | ) 101 | } 102 | } 103 | 104 | render(( 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | ), document.getElementById('example')) 113 | -------------------------------------------------------------------------------- /docs/guides/DynamicRouting.md: -------------------------------------------------------------------------------- 1 | # Dynamic Routing 2 | 3 | React Router is great for small sites like [React.js Training](https://reactjs-training.com) ("React Router brought to you by ...") but it's built with websites like [Facebook](https://www.facebook.com/) and [Twitter](https://twitter.com/) in mind, too. 4 | 5 | The primary concern for large apps is the amount of JavaScript required to boot the app. Large apps should download only the JavaScript required to render the current view. Some people call this "code splitting"–you split your code up into multiple bundles that are loaded on-demand as the user navigates around. 6 | 7 | It's important that changes deep down in the application don't require changes all the way up top as well. For example, adding a route to the photo viewer should not affect the size of the initial JavaScript bundle the user downloads. Neither should it cause merge conflicts as multiple teams have their fingers in the same, big route configuration file. 8 | 9 | A router is the perfect place to handle code splitting: it's responsible for setting up your views. 10 | 11 | React Router does all of its [path matching](/docs/guides/RouteMatching.md) and component fetching asynchronously, which allows you to not only load up the components lazily, *but also lazily load the route configuration*. You really only need one route definition in your initial bundle, the router can resolve the rest on demand. 12 | 13 | Routes may define [`getChildRoutes`](/docs/API.md#getchildrouteslocation-callback), [`getIndexRoute`](/docs/API.md#getindexroutelocation-callback), and [`getComponents`](/docs/API.md#getcomponentsnextstate-callback) methods. These are asynchronous and only called when needed. We call it "gradual matching". React Router will gradually match the URL and fetch only the amount of route configuration and components it needs to match the URL and render. 14 | 15 | Coupled with a smart code splitting tool like [webpack](http://webpack.github.io/), a once tiresome architecture is now simple and declarative. 16 | 17 | ```js 18 | const CourseRoute = { 19 | path: 'course/:courseId', 20 | 21 | getChildRoutes(location, callback) { 22 | require.ensure([], function (require) { 23 | callback(null, [ 24 | require('./routes/Announcements'), 25 | require('./routes/Assignments'), 26 | require('./routes/Grades'), 27 | ]) 28 | }) 29 | }, 30 | 31 | getIndexRoute(location, callback) { 32 | require.ensure([], function (require) { 33 | callback(null, { 34 | component: require('./components/Index'), 35 | }) 36 | }) 37 | }, 38 | 39 | getComponents(nextState, callback) { 40 | require.ensure([], function (require) { 41 | callback(null, require('./components/Course')) 42 | }) 43 | } 44 | } 45 | ``` 46 | 47 | Now go look at what hacks you have in place to do this. Just kidding, I don't want to make you sad right now. 48 | 49 | Run the [huge apps](https://github.com/reactjs/react-router/tree/master/examples/huge-apps) example with your web inspector open and watch code get loaded in as you navigate around the demo. 50 | -------------------------------------------------------------------------------- /modules/RouteUtils.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import warning from './routerWarning' 3 | 4 | function isValidChild(object) { 5 | return object == null || React.isValidElement(object) 6 | } 7 | 8 | export function isReactChildren(object) { 9 | return isValidChild(object) || (Array.isArray(object) && object.every(isValidChild)) 10 | } 11 | 12 | function checkPropTypes(componentName, propTypes, props) { 13 | componentName = componentName || 'UnknownComponent' 14 | 15 | for (const propName in propTypes) { 16 | if (Object.prototype.hasOwnProperty.call(propTypes, propName)) { 17 | const error = propTypes[propName](props, propName, componentName) 18 | 19 | /* istanbul ignore if: error logging */ 20 | if (error instanceof Error) 21 | warning(false, error.message) 22 | } 23 | } 24 | } 25 | 26 | function createRoute(defaultProps, props) { 27 | return { ...defaultProps, ...props } 28 | } 29 | 30 | export function createRouteFromReactElement(element) { 31 | const type = element.type 32 | const route = createRoute(type.defaultProps, element.props) 33 | 34 | if (type.propTypes) 35 | checkPropTypes(type.displayName || type.name, type.propTypes, route) 36 | 37 | if (route.children) { 38 | const childRoutes = createRoutesFromReactChildren(route.children, route) 39 | 40 | if (childRoutes.length) 41 | route.childRoutes = childRoutes 42 | 43 | delete route.children 44 | } 45 | 46 | return route 47 | } 48 | 49 | /** 50 | * Creates and returns a routes object from the given ReactChildren. JSX 51 | * provides a convenient way to visualize how routes in the hierarchy are 52 | * nested. 53 | * 54 | * import { Route, createRoutesFromReactChildren } from 'react-router' 55 | * 56 | * const routes = createRoutesFromReactChildren( 57 | * 58 | * 59 | * 60 | * 61 | * ) 62 | * 63 | * Note: This method is automatically used when you provide children 64 | * to a component. 65 | */ 66 | export function createRoutesFromReactChildren(children, parentRoute) { 67 | const routes = [] 68 | 69 | React.Children.forEach(children, function (element) { 70 | if (React.isValidElement(element)) { 71 | // Component classes may have a static create* method. 72 | if (element.type.createRouteFromReactElement) { 73 | const route = element.type.createRouteFromReactElement(element, parentRoute) 74 | 75 | if (route) 76 | routes.push(route) 77 | } else { 78 | routes.push(createRouteFromReactElement(element)) 79 | } 80 | } 81 | }) 82 | 83 | return routes 84 | } 85 | 86 | /** 87 | * Creates and returns an array of routes from the given object which 88 | * may be a JSX route, a plain object route, or an array of either. 89 | */ 90 | export function createRoutes(routes) { 91 | if (isReactChildren(routes)) { 92 | routes = createRoutesFromReactChildren(routes) 93 | } else if (routes && !Array.isArray(routes)) { 94 | routes = [ routes ] 95 | } 96 | 97 | return routes 98 | } 99 | -------------------------------------------------------------------------------- /modules/__tests__/IndexRoute-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React, { Component } from 'react' 3 | import { render, unmountComponentAtNode } from 'react-dom' 4 | import createHistory from '../createMemoryHistory' 5 | import IndexRoute from '../IndexRoute' 6 | import Router from '../Router' 7 | import Route from '../Route' 8 | 9 | describe('An ', function () { 10 | 11 | class Parent extends Component { 12 | render() { 13 | return
    parent {this.props.children}
    14 | } 15 | } 16 | 17 | class Child extends Component { 18 | render() { 19 | return
    child
    20 | } 21 | } 22 | 23 | let node 24 | beforeEach(function () { 25 | node = document.createElement('div') 26 | }) 27 | 28 | afterEach(function () { 29 | unmountComponentAtNode(node) 30 | }) 31 | 32 | it("renders when its parent's URL matches exactly", function (done) { 33 | render(( 34 | 35 | 36 | 37 | 38 | 39 | ), node, function () { 40 | expect(node.textContent).toEqual('parent child') 41 | done() 42 | }) 43 | }) 44 | 45 | describe('nested deeply in the route hierarchy', function () { 46 | it("renders when its parent's URL matches exactly", function (done) { 47 | render(( 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ), node, function () { 57 | expect(node.textContent).toEqual('parent parent child') 58 | done() 59 | }) 60 | }) 61 | 62 | it('renders when its parents combined pathes match', function (done) { 63 | render(( 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ), node, function () { 73 | expect(node.textContent).toEqual('parent parent child') 74 | done() 75 | }) 76 | }) 77 | 78 | it('renders when its parents combined pathes match, and its direct parent is path less', function (done) { 79 | render(( 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | ), node, function () { 93 | expect(node.textContent).toEqual('parent parent parent parent child') 94 | done() 95 | }) 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /examples/auth-with-shared-root/config/routes.js: -------------------------------------------------------------------------------- 1 | import auth from '../utils/auth.js' 2 | 3 | function redirectToLogin(nextState, replace) { 4 | if (!auth.loggedIn()) { 5 | replace({ 6 | pathname: '/login', 7 | state: { nextPathname: nextState.location.pathname } 8 | }) 9 | } 10 | } 11 | 12 | function redirectToDashboard(nextState, replace) { 13 | if (auth.loggedIn()) { 14 | replace('/') 15 | } 16 | } 17 | 18 | export default { 19 | component: require('../components/App'), 20 | childRoutes: [ 21 | { path: '/logout', 22 | getComponent: (nextState, cb) => { 23 | require.ensure([], (require) => { 24 | cb(null, require('../components/Logout')) 25 | }) 26 | } 27 | }, 28 | { path: '/about', 29 | getComponent: (nextState, cb) => { 30 | require.ensure([], (require) => { 31 | cb(null, require('../components/About')) 32 | }) 33 | } 34 | }, 35 | 36 | { onEnter: redirectToDashboard, 37 | childRoutes: [ 38 | // Unauthenticated routes 39 | // Redirect to dashboard if user is already logged in 40 | { path: '/login', 41 | getComponent: (nextState, cb) => { 42 | require.ensure([], (require) => { 43 | cb(null, require('../components/Login')) 44 | }) 45 | } 46 | } 47 | // ... 48 | ] 49 | }, 50 | 51 | { onEnter: redirectToLogin, 52 | childRoutes: [ 53 | // Protected routes that don't share the dashboard UI 54 | { path: '/user/:id', 55 | getComponent: (nextState, cb) => { 56 | require.ensure([], (require) => { 57 | cb(null, require('../components/User')) 58 | }) 59 | } 60 | } 61 | // ... 62 | ] 63 | }, 64 | 65 | { path: '/', 66 | getComponent: (nextState, cb) => { 67 | // Share the path 68 | // Dynamically load the correct component 69 | if (auth.loggedIn()) { 70 | return require.ensure([], (require) => { 71 | cb(null, require('../components/Dashboard')) 72 | }) 73 | } 74 | return require.ensure([], (require) => { 75 | cb(null, require('../components/Landing')) 76 | }) 77 | }, 78 | indexRoute: { 79 | getComponent: (nextState, cb) => { 80 | // Only load if we're logged in 81 | if (auth.loggedIn()) { 82 | return require.ensure([], (require) => { 83 | cb(null, require('../components/PageOne')) 84 | }) 85 | } 86 | return cb() 87 | } 88 | }, 89 | childRoutes: [ 90 | { onEnter: redirectToLogin, 91 | childRoutes: [ 92 | // Protected nested routes for the dashboard 93 | { path: '/page2', 94 | getComponent: (nextState, cb) => { 95 | require.ensure([], (require) => { 96 | cb(null, require('../components/PageTwo')) 97 | }) 98 | } 99 | } 100 | // ... 101 | ] 102 | } 103 | ] 104 | } 105 | 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /modules/__tests__/_bc-serverRendering-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React, { Component } from 'react' 3 | import { renderToString } from 'react-dom/server' 4 | 5 | import Link from '../Link' 6 | import match from '../match' 7 | import RoutingContext from '../RoutingContext' 8 | import shouldWarn from './shouldWarn' 9 | 10 | describe('v1 server rendering', function () { 11 | 12 | class App extends Component { 13 | render() { 14 | return ( 15 |
    16 |

    App

    17 | About{' '} 18 | Dashboard 19 |
    20 | {this.props.children} 21 |
    22 |
    23 | ) 24 | } 25 | } 26 | 27 | class Dashboard extends Component { 28 | render() { 29 | return ( 30 |
    31 |

    The Dashboard

    32 |
    33 | ) 34 | } 35 | } 36 | 37 | class About extends Component { 38 | render() { 39 | return ( 40 |
    41 |

    About

    42 |
    43 | ) 44 | } 45 | } 46 | 47 | const DashboardRoute = { 48 | path: '/dashboard', 49 | component: Dashboard 50 | } 51 | 52 | const AboutRoute = { 53 | path: '/about', 54 | component: About 55 | } 56 | 57 | const RedirectRoute = { 58 | path: '/company', 59 | onEnter(nextState, replaceState) { 60 | replaceState(null, '/about') 61 | } 62 | } 63 | 64 | const routes = { 65 | path: '/', 66 | component: App, 67 | childRoutes: [ DashboardRoute, AboutRoute, RedirectRoute ] 68 | } 69 | 70 | it('works', function (done) { 71 | shouldWarn('has been renamed') 72 | 73 | match({ routes, location: '/dashboard' }, function (error, redirectLocation, renderProps) { 74 | const string = renderToString( 75 | 76 | ) 77 | expect(string).toMatch(/The Dashboard/) 78 | done() 79 | }) 80 | }) 81 | 82 | it('renders active Links as active', function (done) { 83 | shouldWarn('has been renamed') 84 | 85 | match({ routes, location: '/about' }, function (error, redirectLocation, renderProps) { 86 | const string = renderToString( 87 | 88 | ) 89 | expect(string).toMatch(/about-is-active/) 90 | expect(string).toNotMatch(/dashboard-is-active/) 91 | done() 92 | }) 93 | }) 94 | 95 | it('sends the redirect location', function (done) { 96 | shouldWarn('deprecated') 97 | 98 | match({ routes, location: '/company' }, function (error, redirectLocation) { 99 | expect(redirectLocation).toExist() 100 | expect(redirectLocation.pathname).toEqual('/about') 101 | expect(redirectLocation.search).toEqual('') 102 | expect(redirectLocation.state).toEqual(null) 103 | expect(redirectLocation.action).toEqual('REPLACE') 104 | done() 105 | }) 106 | }) 107 | 108 | it('sends null values when no routes match', function (done) { 109 | match({ routes, location: '/no-match' }, function (error, redirectLocation, state) { 110 | expect(error).toNotExist() 111 | expect(redirectLocation).toNotExist() 112 | expect(state).toNotExist() 113 | done() 114 | }) 115 | }) 116 | 117 | }) 118 | -------------------------------------------------------------------------------- /docs/guides/ServerRendering.md: -------------------------------------------------------------------------------- 1 | # Server Rendering 2 | 3 | Server rendering is a bit different than in a client because you'll want to: 4 | 5 | - Send `500` responses for errors 6 | - Send `30x` responses for redirects 7 | - Fetch data before rendering (and use the router to help you do it) 8 | 9 | To facilitate these needs, you drop one level lower than the [``](/docs/API.md#Router) API with: 10 | 11 | - [`match`](/docs/API.md#match-routes-location-history-options--cb) to match the routes to a location without rendering 12 | - `RouterContext` for synchronous rendering of route components 13 | 14 | It looks something like this with an imaginary JavaScript server: 15 | 16 | ```js 17 | import { renderToString } from 'react-dom/server' 18 | import { match, RouterContext } from 'react-router' 19 | import routes from './routes' 20 | 21 | serve((req, res) => { 22 | // Note that req.url here should be the full URL path from 23 | // the original request, including the query string. 24 | match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { 25 | if (error) { 26 | res.status(500).send(error.message) 27 | } else if (redirectLocation) { 28 | res.redirect(302, redirectLocation.pathname + redirectLocation.search) 29 | } else if (renderProps) { 30 | // You can also check renderProps.components or renderProps.routes for 31 | // your "not found" component or route respectively, and send a 404 as 32 | // below, if you're using a catch-all route. 33 | res.status(200).send(renderToString()) 34 | } else { 35 | res.status(404).send('Not found') 36 | } 37 | }) 38 | }) 39 | ``` 40 | 41 | For data loading, you can use the `renderProps` argument to build whatever convention you want--like adding static `load` methods to your route components, or putting data loading functions on the routes--it's up to you. 42 | 43 | ## Async Routes 44 | 45 | Server rendering works identically when using async routes. However, the client-side rendering needs to be a little different to make sure all of the async behavior has been resolved before the initial render, to avoid a mismatch between the server rendered and client rendered markup. 46 | 47 | On the client, instead of rendering 48 | 49 | ```js 50 | render(, mountNode) 51 | ``` 52 | 53 | You need to do 54 | 55 | ```js 56 | match({ history, routes }, (error, redirectLocation, renderProps) => { 57 | render(, mountNode) 58 | }) 59 | ``` 60 | 61 | ## History Singletons 62 | 63 | Because the server has no DOM available, the history singletons (`browserHistory` and `hashHistory`) do not function on the server. Instead, they will simply return `undefined`. 64 | 65 | You should be sure to only use the history singletons in client code. For React Components, this means using them only in lifecycle functions like `componentDidMount`, but not in `componentWillMount`. Most events, such as clicks, can only happen in the client, as the server has no DOM available to trigger them. So, using the history singletons is a valid option in that case. Knowing what code should run on the server and on the client is important to using React in a universal app, so make sure you're familiar with these concepts even if you aren't using React Router. 66 | 67 | And don't feel discouraged! History singletons are a great convenience method to navigate without setting up `this.context` or when you're not inside of a React component. Simply take care to only use them in places the server will not try to touch. 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router", 3 | "version": "2.4.1", 4 | "description": "A complete routing library for React", 5 | "files": [ 6 | "*.md", 7 | "docs", 8 | "es6", 9 | "lib", 10 | "umd" 11 | ], 12 | "main": "lib/index", 13 | "jsnext:main": "es6/index", 14 | "repository": "reactjs/react-router", 15 | "homepage": "https://github.com/reactjs/react-router#readme", 16 | "bugs": "https://github.com/reactjs/react-router/issues", 17 | "scripts": { 18 | "build": "npm run build-cjs && npm run build-es", 19 | "build-cjs": "rimraf lib && cross-env BABEL_ENV=cjs babel ./modules -d lib --ignore '__tests__'", 20 | "build-es": "rimraf es6 && cross-env BABEL_ENV=es babel ./modules -d es6 --ignore '__tests__'", 21 | "build-umd": "cross-env BABEL_ENV=cjs NODE_ENV=development webpack modules/index.js umd/ReactRouter.js", 22 | "build-min": "cross-env BABEL_ENV=cjs NODE_ENV=production webpack -p modules/index.js umd/ReactRouter.min.js", 23 | "lint": "eslint modules examples", 24 | "start": "cross-env BABEL_ENV=cjs node examples/server.js", 25 | "test": "npm run lint && npm run test-node && npm run test-browser", 26 | "test-browser": "cross-env BABEL_ENV=cjs karma start", 27 | "test-node": "cross-env BABEL_ENV=cjs mocha --compilers js:babel-register tests.node.js" 28 | }, 29 | "authors": [ 30 | "Ryan Florence", 31 | "Michael Jackson" 32 | ], 33 | "license": "MIT", 34 | "dependencies": { 35 | "history": "^2.0.1", 36 | "hoist-non-react-statics": "^1.0.5", 37 | "invariant": "^2.2.1", 38 | "warning": "^2.1.0" 39 | }, 40 | "peerDependencies": { 41 | "react": "^0.14.0 || ^15.0.0" 42 | }, 43 | "devDependencies": { 44 | "babel-cli": "^6.7.5", 45 | "babel-core": "^6.7.6", 46 | "babel-eslint": "^5.0.4", 47 | "babel-loader": "^6.2.4", 48 | "babel-plugin-add-module-exports": "^0.1.2", 49 | "babel-plugin-dev-expression": "^0.2.1", 50 | "babel-preset-es2015": "^6.6.0", 51 | "babel-preset-es2015-loose": "^7.0.0", 52 | "babel-preset-es2015-loose-native-modules": "^1.0.0", 53 | "babel-preset-react": "^6.5.0", 54 | "babel-preset-stage-1": "^6.5.0", 55 | "babel-register": "^6.7.2", 56 | "bundle-loader": "^0.5.4", 57 | "codecov.io": "^0.1.6", 58 | "coveralls": "^2.11.9", 59 | "cross-env": "^1.0.7", 60 | "css-loader": "^0.23.1", 61 | "eslint": "^1.10.3", 62 | "eslint-config-rackt": "^1.1.1", 63 | "eslint-plugin-react": "^3.16.1", 64 | "expect": "^1.18.0", 65 | "express": "^4.13.4", 66 | "express-urlrewrite": "^1.2.0", 67 | "gzip-size": "^3.0.0", 68 | "isparta-loader": "^2.0.0", 69 | "karma": "^0.13.22", 70 | "karma-browserstack-launcher": "^0.1.10", 71 | "karma-chrome-launcher": "^0.2.3", 72 | "karma-coverage": "^0.5.5", 73 | "karma-mocha": "^0.2.2", 74 | "karma-mocha-reporter": "^2.0.1", 75 | "karma-sourcemap-loader": "^0.3.7", 76 | "karma-webpack": "^1.7.0", 77 | "mocha": "^2.4.5", 78 | "pretty-bytes": "^3.0.1", 79 | "qs": "^6.1.0", 80 | "react": "^15.0.0", 81 | "react-addons-css-transition-group": "^15.0.0", 82 | "react-addons-test-utils": "^15.0.0", 83 | "react-dom": "^15.0.0", 84 | "rimraf": "^2.5.2", 85 | "style-loader": "^0.13.1", 86 | "webpack": "^1.12.14", 87 | "webpack-dev-middleware": "^1.6.1" 88 | }, 89 | "tags": [ 90 | "react", 91 | "router" 92 | ], 93 | "keywords": [ 94 | "react", 95 | "react-component", 96 | "routing", 97 | "route", 98 | "routes", 99 | "router" 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /examples/auth-flow/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link, withRouter } from 'react-router' 4 | import auth from './auth' 5 | 6 | const App = React.createClass({ 7 | getInitialState() { 8 | return { 9 | loggedIn: auth.loggedIn() 10 | } 11 | }, 12 | 13 | updateAuth(loggedIn) { 14 | this.setState({ 15 | loggedIn: loggedIn 16 | }) 17 | }, 18 | 19 | componentWillMount() { 20 | auth.onChange = this.updateAuth 21 | auth.login() 22 | }, 23 | 24 | render() { 25 | return ( 26 |
    27 |
      28 |
    • 29 | {this.state.loggedIn ? ( 30 | Log out 31 | ) : ( 32 | Sign in 33 | )} 34 |
    • 35 |
    • About
    • 36 |
    • Dashboard (authenticated)
    • 37 |
    38 | {this.props.children ||

    You are {!this.state.loggedIn && 'not'} logged in.

    } 39 |
    40 | ) 41 | } 42 | }) 43 | 44 | const Dashboard = React.createClass({ 45 | render() { 46 | const token = auth.getToken() 47 | 48 | return ( 49 |
    50 |

    Dashboard

    51 |

    You made it!

    52 |

    {token}

    53 |
    54 | ) 55 | } 56 | }) 57 | 58 | const Login = withRouter( 59 | React.createClass({ 60 | 61 | getInitialState() { 62 | return { 63 | error: false 64 | } 65 | }, 66 | 67 | handleSubmit(event) { 68 | event.preventDefault() 69 | 70 | const email = this.refs.email.value 71 | const pass = this.refs.pass.value 72 | 73 | auth.login(email, pass, (loggedIn) => { 74 | if (!loggedIn) 75 | return this.setState({ error: true }) 76 | 77 | const { location } = this.props 78 | 79 | if (location.state && location.state.nextPathname) { 80 | this.props.router.replace(location.state.nextPathname) 81 | } else { 82 | this.props.router.replace('/') 83 | } 84 | }) 85 | }, 86 | 87 | render() { 88 | return ( 89 |
    90 | 91 | (hint: password1)
    92 | 93 | {this.state.error && ( 94 |

    Bad login information

    95 | )} 96 |
    97 | ) 98 | } 99 | }) 100 | ) 101 | 102 | const About = React.createClass({ 103 | render() { 104 | return

    About

    105 | } 106 | }) 107 | 108 | const Logout = React.createClass({ 109 | componentDidMount() { 110 | auth.logout() 111 | }, 112 | 113 | render() { 114 | return

    You are now logged out

    115 | } 116 | }) 117 | 118 | function requireAuth(nextState, replace) { 119 | if (!auth.loggedIn()) { 120 | replace({ 121 | pathname: '/login', 122 | state: { nextPathname: nextState.location.pathname } 123 | }) 124 | } 125 | } 126 | 127 | render(( 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | ), document.getElementById('example')) 137 | -------------------------------------------------------------------------------- /modules/TransitionUtils.js: -------------------------------------------------------------------------------- 1 | import { loopAsync } from './AsyncUtils' 2 | import warning from './routerWarning' 3 | 4 | function createTransitionHook(hook, route, asyncArity) { 5 | return function (...args) { 6 | hook.apply(route, args) 7 | 8 | if (hook.length < asyncArity) { 9 | let callback = args[args.length - 1] 10 | // Assume hook executes synchronously and 11 | // automatically call the callback. 12 | callback() 13 | } 14 | } 15 | } 16 | 17 | function getEnterHooks(routes) { 18 | return routes.reduce(function (hooks, route) { 19 | if (route.onEnter) 20 | hooks.push(createTransitionHook(route.onEnter, route, 3)) 21 | 22 | return hooks 23 | }, []) 24 | } 25 | 26 | function getChangeHooks(routes) { 27 | return routes.reduce(function (hooks, route) { 28 | if (route.onChange) 29 | hooks.push(createTransitionHook(route.onChange, route, 4)) 30 | return hooks 31 | }, []) 32 | } 33 | 34 | function runTransitionHooks(length, iter, callback) { 35 | if (!length) { 36 | callback() 37 | return 38 | } 39 | 40 | let redirectInfo 41 | function replace(location, deprecatedPathname, deprecatedQuery) { 42 | if (deprecatedPathname) { 43 | warning( 44 | false, 45 | '`replaceState(state, pathname, query) is deprecated; use `replace(location)` with a location descriptor instead. http://tiny.cc/router-isActivedeprecated' 46 | ) 47 | redirectInfo = { 48 | pathname: deprecatedPathname, 49 | query: deprecatedQuery, 50 | state: location 51 | } 52 | 53 | return 54 | } 55 | 56 | redirectInfo = location 57 | } 58 | 59 | loopAsync(length, function (index, next, done) { 60 | iter(index, replace, function (error) { 61 | if (error || redirectInfo) { 62 | done(error, redirectInfo) // No need to continue. 63 | } else { 64 | next() 65 | } 66 | }) 67 | }, callback) 68 | } 69 | 70 | /** 71 | * Runs all onEnter hooks in the given array of routes in order 72 | * with onEnter(nextState, replace, callback) and calls 73 | * callback(error, redirectInfo) when finished. The first hook 74 | * to use replace short-circuits the loop. 75 | * 76 | * If a hook needs to run asynchronously, it may use the callback 77 | * function. However, doing so will cause the transition to pause, 78 | * which could lead to a non-responsive UI if the hook is slow. 79 | */ 80 | export function runEnterHooks(routes, nextState, callback) { 81 | const hooks = getEnterHooks(routes) 82 | return runTransitionHooks(hooks.length, (index, replace, next) => { 83 | hooks[index](nextState, replace, next) 84 | }, callback) 85 | } 86 | 87 | /** 88 | * Runs all onChange hooks in the given array of routes in order 89 | * with onChange(prevState, nextState, replace, callback) and calls 90 | * callback(error, redirectInfo) when finished. The first hook 91 | * to use replace short-circuits the loop. 92 | * 93 | * If a hook needs to run asynchronously, it may use the callback 94 | * function. However, doing so will cause the transition to pause, 95 | * which could lead to a non-responsive UI if the hook is slow. 96 | */ 97 | export function runChangeHooks(routes, state, nextState, callback) { 98 | const hooks = getChangeHooks(routes) 99 | return runTransitionHooks(hooks.length, (index, replace, next) => { 100 | hooks[index](state, nextState, replace, next) 101 | }, callback) 102 | } 103 | 104 | /** 105 | * Runs all onLeave hooks in the given array of routes in order. 106 | */ 107 | export function runLeaveHooks(routes) { 108 | for (let i = 0, len = routes.length; i < len; ++i) 109 | if (routes[i].onLeave) 110 | routes[i].onLeave.call(routes[i]) 111 | } 112 | -------------------------------------------------------------------------------- /modules/RouterContext.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant' 2 | import React from 'react' 3 | 4 | import deprecateObjectProperties from './deprecateObjectProperties' 5 | import getRouteParams from './getRouteParams' 6 | import { isReactChildren } from './RouteUtils' 7 | import warning from './routerWarning' 8 | 9 | const { array, func, object } = React.PropTypes 10 | 11 | /** 12 | * A renders the component tree for a given router state 13 | * and sets the history object and the current location in context. 14 | */ 15 | const RouterContext = React.createClass({ 16 | 17 | propTypes: { 18 | history: object, 19 | router: object.isRequired, 20 | location: object.isRequired, 21 | routes: array.isRequired, 22 | params: object.isRequired, 23 | components: array.isRequired, 24 | createElement: func.isRequired 25 | }, 26 | 27 | getDefaultProps() { 28 | return { 29 | createElement: React.createElement 30 | } 31 | }, 32 | 33 | childContextTypes: { 34 | history: object, 35 | location: object.isRequired, 36 | router: object.isRequired 37 | }, 38 | 39 | getChildContext() { 40 | let { router, history, location } = this.props 41 | if (!router) { 42 | warning(false, '`` expects a `router` rather than a `history`') 43 | 44 | router = { 45 | ...history, 46 | setRouteLeaveHook: history.listenBeforeLeavingRoute 47 | } 48 | delete router.listenBeforeLeavingRoute 49 | } 50 | 51 | if (__DEV__) { 52 | location = deprecateObjectProperties(location, '`context.location` is deprecated, please use a route component\'s `props.location` instead. http://tiny.cc/router-accessinglocation') 53 | } 54 | 55 | return { history, location, router } 56 | }, 57 | 58 | createElement(component, props) { 59 | return component == null ? null : this.props.createElement(component, props) 60 | }, 61 | 62 | render() { 63 | const { history, location, routes, params, components } = this.props 64 | let element = null 65 | 66 | if (components) { 67 | element = components.reduceRight((element, components, index) => { 68 | if (components == null) 69 | return element // Don't create new children; use the grandchildren. 70 | 71 | const route = routes[index] 72 | const routeParams = getRouteParams(route, params) 73 | const props = { 74 | history, 75 | location, 76 | params, 77 | route, 78 | routeParams, 79 | routes 80 | } 81 | 82 | if (isReactChildren(element)) { 83 | props.children = element 84 | } else if (element) { 85 | for (const prop in element) 86 | if (Object.prototype.hasOwnProperty.call(element, prop)) 87 | props[prop] = element[prop] 88 | } 89 | 90 | if (typeof components === 'object') { 91 | const elements = {} 92 | 93 | for (const key in components) { 94 | if (Object.prototype.hasOwnProperty.call(components, key)) { 95 | // Pass through the key as a prop to createElement to allow 96 | // custom createElement functions to know which named component 97 | // they're rendering, for e.g. matching up to fetched data. 98 | elements[key] = this.createElement(components[key], { 99 | key, ...props 100 | }) 101 | } 102 | } 103 | 104 | return elements 105 | } 106 | 107 | return this.createElement(components, props) 108 | }, element) 109 | } 110 | 111 | invariant( 112 | element === null || element === false || React.isValidElement(element), 113 | 'The root route must render a single element' 114 | ) 115 | 116 | return element 117 | } 118 | 119 | }) 120 | 121 | export default RouterContext 122 | -------------------------------------------------------------------------------- /docs/guides/ComponentLifecycle.md: -------------------------------------------------------------------------------- 1 | # Component Lifecycle 2 | 3 | It's important to understand which lifecycle hooks are going to be called 4 | on your route components to implement lots of different functionality in 5 | your app. The most common thing is fetching data. 6 | 7 | There is no difference in the lifecycle of a component in the router as 8 | just React itself. Let's peel away the idea of routes, and just think 9 | about the components being rendered at different URLs. 10 | 11 | Consider this route config: 12 | 13 | ```js 14 | 15 | 16 | 17 | 18 | 19 | ``` 20 | 21 | ### Lifecycle hooks when routing 22 | 23 | 1. Lets say the user enters the app at `/`. 24 | 25 | | Component | Lifecycle Hooks called | 26 | |-----------|------------------------| 27 | | App | (2) `componentDidMount` | 28 | | Home | (1) `componentDidMount` | 29 | | Invoice | N/A | 30 | | Account | N/A | 31 | 32 | 2. Now they navigate from `/` to `/invoices/123` 33 | 34 | | Component | Lifecycle Hooks called | 35 | |-----------|------------------------| 36 | | App | (1) `componentWillReceiveProps`, (4) `componentDidUpdate` | 37 | | Home | (2) `componentWillUnmount` | 38 | | Invoice | (3) `componentDidMount` | 39 | | Account | N/A | 40 | 41 | - `App` gets `componentWillReceiveProps` and `componentDidUpdate` because it 42 | stayed rendered but just received new props from the router (like: 43 | `children`, `params`, `location`, etc.) 44 | - `Home` is no longer rendered, so it gets unmounted. 45 | - `Invoice` is mounted for the first time. 46 | 47 | 48 | 3. Now they navigate from `/invoices/123` to `/invoices/789` 49 | 50 | | Component | Lifecycle Hooks called | 51 | |-----------|------------------------| 52 | | App | (1) `componentWillReceiveProps`, (4) `componentDidUpdate` | 53 | | Home | N/A | 54 | | Invoice | (2) `componentWillReceiveProps`, (3) `componentDidUpdate` | 55 | | Account | N/A | 56 | 57 | All the components that were mounted before, are still mounted, they 58 | just receive new props from the router. 59 | 60 | 4. Now they navigate from `/invoices/789` to `/accounts/123` 61 | 62 | | Component | Lifecycle Hooks called | 63 | |-----------|------------------------| 64 | | App | (1) `componentWillReceiveProps`, (4) `componentDidUpdate` | 65 | | Home | N/A | 66 | | Invoice | (2) `componentWillUnmount` | 67 | | Account | (3) `componentDidMount` | 68 | 69 | ### Fetching Data 70 | 71 | While there are other ways to fetch data with the router, the simplest 72 | way is to simply use the lifecycle hooks of your components and keep 73 | that data in state. Now that we understand the lifecycle of components 74 | when changing routes, we can implement simple data fetching inside of 75 | `Invoice`. 76 | 77 | ```js 78 | let Invoice = React.createClass({ 79 | 80 | getInitialState () { 81 | return { 82 | invoice: null 83 | } 84 | }, 85 | 86 | componentDidMount () { 87 | // fetch data initially in scenario 2 from above 88 | this.fetchInvoice() 89 | }, 90 | 91 | componentDidUpdate (prevProps) { 92 | // respond to parameter change in scenario 3 93 | let oldId = prevProps.params.invoiceId 94 | let newId = this.props.params.invoiceId 95 | if (newId !== oldId) 96 | this.fetchInvoice() 97 | }, 98 | 99 | componentWillUnmount () { 100 | // allows us to ignore an inflight request in scenario 4 101 | this.ignoreLastFetch = true 102 | }, 103 | 104 | fetchInvoice () { 105 | let url = `/api/invoices/${this.props.params.invoiceId}` 106 | this.request = fetch(url, (err, data) => { 107 | if (!this.ignoreLastFetch) 108 | this.setState({ invoice: data.invoice }) 109 | }) 110 | }, 111 | 112 | render () { 113 | return 114 | } 115 | 116 | }) 117 | ``` 118 | -------------------------------------------------------------------------------- /examples/nested-animations/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group' 4 | import { browserHistory, Router, Route, Link } from 'react-router' 5 | import './app.css' 6 | 7 | class App extends Component { 8 | render() { 9 | const { pathname } = this.props.location 10 | 11 | // Only take the first-level part of the path as key, instead of the whole path. 12 | const key = pathname.split('/')[1] || 'root' 13 | 14 | return ( 15 |
    16 |
      17 |
    • Page 1
    • 18 |
    • Page 2
    • 19 |
    20 | 24 | {React.cloneElement(this.props.children ||
    , { key: key })} 25 | 26 |
    27 | ) 28 | } 29 | } 30 | 31 | class Page1 extends Component { 32 | render() { 33 | const { pathname } = this.props.location 34 | 35 | return ( 36 |
    37 |

    Page 1

    38 |
      39 |
    • Tab 1
    • 40 |
    • Tab 2
    • 41 |
    42 | 46 | {React.cloneElement(this.props.children ||
    , { key: pathname })} 47 | 48 |
    49 | ) 50 | } 51 | } 52 | 53 | class Page2 extends Component { 54 | render() { 55 | const { pathname } = this.props.location 56 | 57 | return ( 58 |
    59 |

    Page 2

    60 |
      61 |
    • Tab 1
    • 62 |
    • Tab 2
    • 63 |
    64 | 68 | {React.cloneElement(this.props.children ||
    , { key: pathname })} 69 | 70 |
    71 | ) 72 | } 73 | } 74 | 75 | class Tab1 extends Component { 76 | render() { 77 | return ( 78 |
    79 |

    Tab 1

    80 |

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    81 |
    82 | ) 83 | } 84 | } 85 | 86 | class Tab2 extends Component { 87 | render() { 88 | return ( 89 |
    90 |

    Tab 2

    91 |

    Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    92 |
    93 | ) 94 | } 95 | } 96 | 97 | render(( 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | ), document.getElementById('example')) 111 | -------------------------------------------------------------------------------- /docs/guides/testing.md: -------------------------------------------------------------------------------- 1 | React Router Testing With Jest 2 | ==================== 3 | Testing has become much easier since React Router version 1.x. For Testing prior React Router versions, see [Old testing docs](https://github.com/reactjs/react-router/blob/57543eb41ce45b994a29792d77c86cc10b51eac9/docs/guides/testing.md). 4 | 5 | It is recommended that you read the following two tutorials prior: 6 | - [Jest Getting Started docs](https://facebook.github.io/jest/docs/getting-started.html) 7 | - [Jest ReactJS docs](https://facebook.github.io/jest/docs/tutorial-react.html) 8 | - [ReactJS TestUtils docs](https://facebook.github.io/react/docs/test-utils.html) 9 | 10 | Testing with React-Router 1.x should just work. But if you are having issues see the following. Many users had issues when upgrading prior setups from react-router 0.x. 11 | 12 | Updating from testing setup from React-Router 0.x to 1.x 13 | ---------------------------------------------- 14 | Firstly, ensure you are using at least the following versions of each package. 15 | - `"react": "^0.14.0"` 16 | - `"react-dom": "^0.14.0"` 17 | - `"react-router": "^1.0.0"` 18 | - `"react-addons-test-utils": "^0.14.0"` 19 | - `"jest": "^0.1.40"` 20 | - `"jest-cli": "^0.10.0"` 21 | - `"babel-jest": "^10.0.1"` 22 | 23 | Also, make sure you are using node 4.x 24 | 25 | In prior setups, react-tools was needed. This is no longer the case. You will need to remove it from your package.json and environment. 26 | 27 | ```json 28 | "react-tools": "~0.13.3", 29 | ``` 30 | 31 | Lastly, anywhere you have the following: 32 | 33 | ```js 34 | var React = require('react/addons') 35 | var TestUtils = React.addons.TestUtils 36 | ``` 37 | 38 | needs to be replaced with this: 39 | 40 | ```js 41 | import React from 'react' 42 | import { render } from 'react-dom' 43 | import TestUtils from 'react-addons-test-utils' 44 | ``` 45 | 46 | Make sure you do an npm clean, install, etc. and make sure you add react-addons-test-utils and react-dom to your unmocked paths. 47 | 48 | ```json 49 | ... 50 | "unmockedModulePathPatterns": [ 51 | "./node_modules/react", 52 | "./node_modules/react-dom", 53 | "./node_modules/react-addons-test-utils", 54 | ], 55 | ... 56 | 57 | ``` 58 | 59 | Lastly ensure you are using babel-jest for the script preproccessor: 60 | 61 | ```js 62 | ... 63 | "scriptPreprocessor": "./node_modules/babel-jest", 64 | ... 65 | ``` 66 | 67 | 68 | Example: 69 | ---------------------------------------------- 70 | 71 | A component: 72 | 73 | ```js 74 | //../components/BasicPage.js 75 | 76 | import React, { Component, PropTypes } from 'react' 77 | import { Button } from 'react-bootstrap' 78 | import { Link } from 'react-router' 79 | 80 | 81 | export default class BasicPage extends Component { 82 | static propTypes = { 83 | authenticated: PropTypes.bool 84 | } 85 | 86 | render() { 87 | return ( 88 |
    89 | { this.props.authenticated ? ( 90 |
    91 | 92 |
    93 | ) : ( 94 |
    95 | 96 |
    97 | ) 98 | } 99 |
    100 | ) 101 | } 102 | } 103 | ``` 104 | 105 | The test for that component: 106 | 107 | ```js 108 | //../components/__tests__/BasicPage-test.js 109 | 110 | jest.unmock('../BasicPage') 111 | 112 | import TestUtils from 'react-addons-test-utils' 113 | import ReactDOM from 'react-dom' 114 | import React from 'react' 115 | import BasicPage from '../BasicPage' 116 | 117 | describe('BasicPage', function() { 118 | 119 | it('renders the Login button if not logged in', function() { 120 | let page = TestUtils.renderIntoDocument() 121 | let button = TestUtils.findRenderedDOMComponentWithTag(page, 'button') 122 | expect(ReactDOM.findDOMNode(button).textContent).toBe('Login') 123 | }) 124 | 125 | it('renders the Account button if logged in', function() { 126 | let page = TestUtils.renderIntoDocument() 127 | let button = TestUtils.findRenderedDOMComponentWithTag(page, 'button') 128 | expect(ReactDOM.findDOMNode(button).textContent).toBe('Your Account') 129 | }) 130 | }) 131 | ``` 132 | -------------------------------------------------------------------------------- /modules/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import warning from './routerWarning' 3 | import { routerShape } from './PropTypes' 4 | 5 | const { bool, object, string, func, oneOfType } = React.PropTypes 6 | 7 | function isLeftClickEvent(event) { 8 | return event.button === 0 9 | } 10 | 11 | function isModifiedEvent(event) { 12 | return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) 13 | } 14 | 15 | // TODO: De-duplicate against hasAnyProperties in createTransitionManager. 16 | function isEmptyObject(object) { 17 | for (const p in object) 18 | if (Object.prototype.hasOwnProperty.call(object, p)) 19 | return false 20 | 21 | return true 22 | } 23 | 24 | function createLocationDescriptor(to, { query, hash, state }) { 25 | if (query || hash || state) { 26 | return { pathname: to, query, hash, state } 27 | } 28 | 29 | return to 30 | } 31 | 32 | /** 33 | * A is used to create an
    element that links to a route. 34 | * When that route is active, the link gets the value of its 35 | * activeClassName prop. 36 | * 37 | * For example, assuming you have the following route: 38 | * 39 | * 40 | * 41 | * You could use the following component to link to that route: 42 | * 43 | * 44 | * 45 | * Links may pass along location state and/or query string parameters 46 | * in the state/query props, respectively. 47 | * 48 | * 49 | */ 50 | const Link = React.createClass({ 51 | 52 | contextTypes: { 53 | router: routerShape 54 | }, 55 | 56 | propTypes: { 57 | to: oneOfType([ string, object ]).isRequired, 58 | query: object, 59 | hash: string, 60 | state: object, 61 | activeStyle: object, 62 | activeClassName: string, 63 | onlyActiveOnIndex: bool.isRequired, 64 | onClick: func, 65 | target: string 66 | }, 67 | 68 | getDefaultProps() { 69 | return { 70 | onlyActiveOnIndex: false, 71 | style: {} 72 | } 73 | }, 74 | 75 | handleClick(event) { 76 | let allowTransition = true 77 | 78 | if (this.props.onClick) 79 | this.props.onClick(event) 80 | 81 | if (isModifiedEvent(event) || !isLeftClickEvent(event)) 82 | return 83 | 84 | if (event.defaultPrevented === true) 85 | allowTransition = false 86 | 87 | // If target prop is set (e.g. to "_blank") let browser handle link. 88 | /* istanbul ignore if: untestable with Karma */ 89 | if (this.props.target) { 90 | if (!allowTransition) 91 | event.preventDefault() 92 | 93 | return 94 | } 95 | 96 | event.preventDefault() 97 | 98 | if (allowTransition) { 99 | const { to, query, hash, state } = this.props 100 | const location = createLocationDescriptor(to, { query, hash, state }) 101 | 102 | this.context.router.push(location) 103 | } 104 | }, 105 | 106 | render() { 107 | const { to, query, hash, state, activeClassName, activeStyle, onlyActiveOnIndex, ...props } = this.props 108 | warning( 109 | !(query || hash || state), 110 | 'the `query`, `hash`, and `state` props on `` are deprecated, use `. http://tiny.cc/router-isActivedeprecated' 111 | ) 112 | 113 | // Ignore if rendered outside the context of router, simplifies unit testing. 114 | const { router } = this.context 115 | 116 | if (router) { 117 | const location = createLocationDescriptor(to, { query, hash, state }) 118 | props.href = router.createHref(location) 119 | 120 | if (activeClassName || (activeStyle != null && !isEmptyObject(activeStyle))) { 121 | if (router.isActive(location, onlyActiveOnIndex)) { 122 | if (activeClassName) { 123 | if (props.className) { 124 | props.className += ` ${activeClassName}` 125 | } else { 126 | props.className = activeClassName 127 | } 128 | } 129 | 130 | if (activeStyle) 131 | props.style = { ...props.style, ...activeStyle } 132 | } 133 | } 134 | } 135 | 136 | return 137 | } 138 | 139 | }) 140 | 141 | export default Link 142 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0*/ 2 | var webpack = require('webpack') 3 | var path = require('path') 4 | 5 | module.exports = function (config) { 6 | if (process.env.RELEASE) 7 | config.singleRun = true 8 | 9 | var customLaunchers = { 10 | // Browsers to run on BrowserStack. 11 | BS_Chrome: { 12 | base: 'BrowserStack', 13 | os: 'Windows', 14 | os_version: '10', 15 | browser: 'chrome', 16 | browser_version: '47.0' 17 | }, 18 | BS_Firefox: { 19 | base: 'BrowserStack', 20 | os: 'Windows', 21 | os_version: '10', 22 | browser: 'firefox', 23 | browser_version: '43.0' 24 | }, 25 | BS_Safari: { 26 | base: 'BrowserStack', 27 | os: 'OS X', 28 | os_version: 'El Capitan', 29 | browser: 'safari', 30 | browser_version: '9.0' 31 | }, 32 | BS_MobileSafari: { 33 | base: 'BrowserStack', 34 | os: 'ios', 35 | os_version: '8.3', 36 | browser: 'iphone', 37 | real_mobile: false 38 | }, 39 | BS_MobileSafari: { 40 | base: 'BrowserStack', 41 | os: 'ios', 42 | os_version: '9.1', 43 | browser: 'iphone', 44 | real_mobile: false 45 | }, 46 | BS_InternetExplorer10: { 47 | base: 'BrowserStack', 48 | os: 'Windows', 49 | os_version: '8', 50 | browser: 'ie', 51 | browser_version: '10.0' 52 | }, 53 | BS_InternetExplorer11: { 54 | base: 'BrowserStack', 55 | os: 'Windows', 56 | os_version: '10', 57 | browser: 'ie', 58 | browser_version: '11.0' 59 | }, 60 | 61 | // The ancient Travis Chrome that most projects use in CI. 62 | ChromeCi: { 63 | base: 'Chrome', 64 | flags: [ '--no-sandbox' ] 65 | } 66 | } 67 | 68 | var isCi = process.env.CONTINUOUS_INTEGRATION === 'true' 69 | var runCoverage = process.env.COVERAGE === 'true' || isCi 70 | 71 | var coverageLoaders = [] 72 | var coverageReporters = [] 73 | 74 | if (runCoverage) { 75 | coverageLoaders.push({ 76 | test: /\.js$/, 77 | include: path.resolve('modules/'), 78 | exclude: /__tests__/, 79 | loader: 'isparta' 80 | }) 81 | 82 | coverageReporters.push('coverage') 83 | } 84 | 85 | config.set({ 86 | customLaunchers: customLaunchers, 87 | 88 | browsers: [ 'Chrome' ], 89 | frameworks: [ 'mocha' ], 90 | reporters: [ 'mocha' ].concat(coverageReporters), 91 | 92 | files: [ 93 | 'tests.webpack.js' 94 | ], 95 | 96 | preprocessors: { 97 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ] 98 | }, 99 | 100 | webpack: { 101 | devtool: 'inline-source-map', 102 | module: { 103 | loaders: [ 104 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' } 105 | ].concat(coverageLoaders) 106 | }, 107 | plugins: [ 108 | new webpack.DefinePlugin({ 109 | 'process.env.NODE_ENV': JSON.stringify('test') 110 | }) 111 | ] 112 | }, 113 | 114 | webpackServer: { 115 | noInfo: true 116 | }, 117 | 118 | coverageReporter: { 119 | reporters: [ 120 | { type: 'html', subdir: 'html' }, 121 | { type: 'lcovonly', subdir: '.' } 122 | ] 123 | } 124 | }) 125 | 126 | if (process.env.USE_CLOUD) { 127 | config.browsers = Object.keys(customLaunchers) 128 | config.reporters[0] = 'dots' 129 | config.browserDisconnectTimeout = 10000 130 | config.browserDisconnectTolerance = 3 131 | config.browserNoActivityTimeout = 30000 132 | config.captureTimeout = 120000 133 | 134 | if (process.env.TRAVIS) { 135 | var buildLabel = 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')' 136 | 137 | config.browserStack = { 138 | username: process.env.BROWSER_STACK_USERNAME, 139 | accessKey: process.env.BROWSER_STACK_ACCESS_KEY, 140 | pollingTimeout: 10000, 141 | startTunnel: true, 142 | project: 'react-router', 143 | build: buildLabel, 144 | name: process.env.TRAVIS_JOB_NUMBER 145 | } 146 | 147 | config.singleRun = true 148 | } else { 149 | config.browserStack = { 150 | username: process.env.BROWSER_STACK_USERNAME, 151 | accessKey: process.env.BROWSER_STACK_ACCESS_KEY, 152 | pollingTimeout: 10000, 153 | startTunnel: true 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /docs/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ### How do I add `this.props.router` to my component? 4 | 5 | You need to wrap your component using `withRouter` to make the router object available to you. 6 | 7 | ```js 8 | const Component = withRouter( 9 | React.createClass({ 10 | //... 11 | }) 12 | ) 13 | ``` 14 | 15 | 16 | ### Getting the previous location 17 | 18 | ```js 19 | 20 | {/* ... other routes */} 21 | 22 | 23 | const App = React.createClass({ 24 | getInitialState() { 25 | return { showBackButton: false } 26 | }, 27 | 28 | componentWillReceiveProps(nextProps) { 29 | const routeChanged = nextProps.location !== this.props.location 30 | this.setState({ showBackButton: routeChanged }) 31 | } 32 | }) 33 | ``` 34 | 35 | 36 | ### Component won't render 37 | 38 | Route matching happens in the order they are defined (think `if/else if` statement). In this case, `/about/me` will show the `` component because `/about/me` matches the first route. You need to reorder your routes if this happens. `` will never be reachable: 39 | 40 | ```js 41 | 42 | 43 | 44 | 45 | ``` 46 | 47 | `About` is now reachable: 48 | 49 | ```js 50 | 51 | 52 | 53 | 54 | ``` 55 | 56 | 57 | ### Parent path does not show as active 58 | 59 | If your routes look like: 60 | 61 | ```js 62 | 63 | 64 | 65 | 66 | ``` 67 | 68 | Then the path `/widgets` will not be considered active when the current path is something like `/widgets/3`. This is because React Router looks at parent _routes_ rather than parent _paths_ to determine active state. To make the path `/widgets` active when the current path is `/widgets/3`, you need to declare your routes as: 69 | 70 | ```js 71 | 72 | 73 | 74 | 75 | 76 | 77 | ``` 78 | 79 | As an additional benefit, this also removes the duplication in declaring route paths. 80 | 81 | 82 | ### "Required prop was not specified" on route components 83 | 84 | You might see this if you are using `React.cloneElement` to inject props into route components from their parents. If you see this, remove `isRequired` from `propTypes` for those props. This happens because React validates `propTypes` when the element is created rather than when it is mounted. For more details, see [facebook/react#4494](https://github.com/facebook/react/issues/4494#issuecomment-125068868). 85 | 86 | You should generally attempt to use this pattern as sparingly as possible. In general, it's best practice to minimize data dependencies between route components. 87 | 88 | 89 | ### Passing additional values into route components 90 | 91 | There are multiple ways to do this depending on what you want specifically. 92 | 93 | #### Declare properties on the route 94 | 95 | You can define additional props on `` or on the plain route: 96 | 97 | ```js 98 | 99 | ``` 100 | 101 | These properties will then be available on `this.props.route` on the route component, such as with `this.props.route.foo` above. 102 | 103 | #### Inject props to all routes via middleware 104 | 105 | You can define a middleware that injects additional props into each route component: 106 | 107 | ```js 108 | const useExtraProps = { 109 | renderRouteComponent: child => React.cloneElement(child, extraProps) 110 | } 111 | ``` 112 | 113 | You can then use this middleware with: 114 | 115 | ```js 116 | 121 | ``` 122 | 123 | #### Use a top-level context provider 124 | 125 | You can export React context on a top-level provider component, then access this data throughout the tree on rendered components. 126 | 127 | ```js 128 | 129 | 130 | 131 | ``` 132 | 133 | 134 | ### `