├── tests.node.js ├── .babelrc ├── modules ├── __tests__ │ ├── .eslintrc │ ├── resetHash.js │ ├── execSteps.js │ ├── getParamNames-test.js │ ├── IndexRedirect-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 │ ├── formatPattern-test.js │ └── RouterContext-test.js ├── hashHistory.js ├── browserHistory.js ├── routerWarning.js ├── IndexLink.js ├── createRouterHistory.js ├── useRouterHistory.js ├── RoutingContext.js ├── History.js ├── getRouteParams.js ├── createMemoryHistory.js ├── RouterUtils.js ├── RouteContext.js ├── getComponents.js ├── index.js ├── PropTypes.js ├── useRoutes.js ├── deprecateObjectProperties.js ├── Route.js ├── IndexRedirect.js ├── IndexRoute.js ├── AsyncUtils.js ├── match.js ├── Lifecycle.js ├── computeChangedRoutes.js ├── Redirect.js ├── RouteUtils.js ├── isActive.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 │ └── 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 ├── server.js ├── index.html └── webpack.config.js ├── .eslintrc ├── 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 ├── tests.webpack.js ├── webpack.config.js ├── LICENSE.md ├── ISSUE_TEMPLATE.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── package.json └── CHANGES.md /tests.node.js: -------------------------------------------------------------------------------- 1 | import './modules/__tests__/serverRendering-test' 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all", 4 | "plugins": [ 5 | "dev-expression" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /modules/routerWarning.js: -------------------------------------------------------------------------------- 1 | import warning from 'warning' 2 | 3 | export default function routerWarning(falseToWarn, message, ...args) { 4 | message = `[react-router] ${message}` 5 | warning(falseToWarn, message, ...args) 6 | } 7 | -------------------------------------------------------------------------------- /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 PageOne = React.createClass({ 4 | render() { 5 | return

Page Two! Wooo!

6 | } 7 | }) 8 | 9 | export default PageOne 10 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Grades/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'grades', 3 | getComponent(location, 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(location, 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(location, 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(location, 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(location, 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(location, 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/huge-apps/routes/Profile/components/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class Profile extends React.Component { 4 | render() { 5 | return ( 6 |
7 |

Profile

8 |
9 | ) 10 | } 11 | } 12 | 13 | module.exports = Profile 14 | -------------------------------------------------------------------------------- /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(location, 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/Course/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class Dashboard extends React.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 from 'react' 2 | 3 | class Messages extends React.Component { 4 | 5 | render() { 6 | return ( 7 |
8 |

Messages

9 |
10 | ) 11 | } 12 | 13 | } 14 | 15 | module.exports = Messages 16 | -------------------------------------------------------------------------------- /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/huge-apps/routes/Course/routes/Assignments/components/Assignments.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class Assignments extends React.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/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/Announcements/components/Announcements.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class Announcements extends React.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/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/auth-flow/index.html: -------------------------------------------------------------------------------- 1 | 2 | Authentication Flow Example 3 | 4 | 5 | 6 |

React Router Examples / Auth Flow

7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 from 'react' 3 | import Dashboard from './Dashboard' 4 | import GlobalNav from './GlobalNav' 5 | 6 | class App extends React.Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 | {this.props.children || } 13 |
14 |
15 | ) 16 | } 17 | } 18 | 19 | module.exports = App 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - stable 5 | env: 6 | - 7 | - SKIP_BC=1 FAIL_ON_WARNINGS=1 8 | cache: 9 | directories: 10 | - node_modules 11 | before_install: 12 | - export CHROME_BIN=chromium-browser 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | after_success: 16 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 17 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 18 | branches: 19 | only: 20 | - master 21 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Calendar/components/Calendar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class Calendar extends React.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 | -------------------------------------------------------------------------------- /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(location, cb) { 15 | require.ensure([], (require) => { 16 | cb(null, require('./components/Course')) 17 | }) 18 | } 19 | } 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/routes/Assignments/routes/Assignment/components/Assignment.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React from 'react' 3 | 4 | class Assignment extends React.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Announcements/routes/Announcement/components/Announcement.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React from 'react' 3 | 4 | class Announcement extends React.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 | -------------------------------------------------------------------------------- /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(location, 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(location, cb) { 13 | require.ensure([], (require) => { 14 | cb(null, { 15 | sidebar: require('./components/Sidebar'), 16 | main: require('./components/Announcements') 17 | }) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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 './PropTypes' 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 from 'react' 3 | 4 | class Grades extends React.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 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0*/ 2 | 3 | if (process.env.FAIL_ON_WARNINGS) { 4 | console.error = function (msg) { 5 | // grant me the serenity to accept the things I cannot change, 6 | if (msg.match(/Warning: the query argument to createHref is deprecated; use a location descriptor instead/)) 7 | return 8 | else 9 | throw new Error(msg) 10 | } 11 | } 12 | 13 | const context = require.context('./modules', true, /-test\.js$/) 14 | 15 | context.keys().forEach(key => { 16 | if (process.env.SKIP_BC) { 17 | if (!key.match(/_bc/)) { 18 | context(key) 19 | } 20 | } else { 21 | context(key) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /docs/guides/NavigatingOutsideOfComponents.md: -------------------------------------------------------------------------------- 1 | # Navigating Outside of Components 2 | 3 | While you can use `this.context.router` 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/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 from 'react' 3 | import { Link } from 'react-router' 4 | 5 | class Sidebar extends React.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 | -------------------------------------------------------------------------------- /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 = React.createClass({ 7 | 8 | contextTypes: { 9 | router: React.PropTypes.object 10 | }, 11 | 12 | componentDidMount() { 13 | this.context.router.setRouteLeaveHook(this.props.route, this.routerWillLeave) 14 | }, 15 | 16 | routerWillLeave(nextLocation) { 17 | // return false to prevent a transition w/o prompting the user, 18 | // or return a string to allow the user to decide: 19 | if (!this.state.isSaved) 20 | return 'Your work is not saved! Are you sure you want to leave?' 21 | }, 22 | 23 | // ... 24 | 25 | }) 26 | ``` 27 | -------------------------------------------------------------------------------- /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/huge-apps/routes/Course/routes/Announcements/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React from 'react' 3 | import { Link } from 'react-router' 4 | 5 | class AnnouncementsSidebar extends React.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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/getComponents.js: -------------------------------------------------------------------------------- 1 | import { mapAsync } from './AsyncUtils' 2 | 3 | function getComponentsForRoute(location, route, callback) { 4 | if (route.component || route.components) { 5 | callback(null, route.component || route.components) 6 | } else if (route.getComponent) { 7 | route.getComponent(location, callback) 8 | } else if (route.getComponents) { 9 | route.getComponents(location, callback) 10 | } else { 11 | callback() 12 | } 13 | } 14 | 15 | /** 16 | * Asynchronously fetches all components needed for the given router 17 | * state and calls callback(error, components) when finished. 18 | * 19 | * Note: This operation may finish synchronously if no routes have an 20 | * asynchronous getComponents method. 21 | */ 22 | function getComponents(nextState, callback) { 23 | mapAsync(nextState.routes, function (route, index, callback) { 24 | getComponentsForRoute(nextState.location, route, callback) 25 | }, callback) 26 | } 27 | 28 | export default getComponents 29 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | React Router Examples 3 | 4 | 5 |

React Router Examples

6 | 22 | -------------------------------------------------------------------------------- /examples/huge-apps/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | class Dashboard extends React.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/index.js: -------------------------------------------------------------------------------- 1 | /* components */ 2 | export Router from './Router' 3 | export Link from './Link' 4 | export IndexLink from './IndexLink' 5 | 6 | /* components (configuration) */ 7 | export IndexRedirect from './IndexRedirect' 8 | export IndexRoute from './IndexRoute' 9 | export Redirect from './Redirect' 10 | export Route from './Route' 11 | 12 | /* mixins */ 13 | export History from './History' 14 | export Lifecycle from './Lifecycle' 15 | export RouteContext from './RouteContext' 16 | 17 | /* utils */ 18 | export useRoutes from './useRoutes' 19 | export { createRoutes } from './RouteUtils' 20 | export RouterContext from './RouterContext' 21 | export RoutingContext from './RoutingContext' 22 | export PropTypes from './PropTypes' 23 | export match from './match' 24 | export useRouterHistory from './useRouterHistory' 25 | export { formatPattern } from './PatternUtils' 26 | 27 | /* histories */ 28 | export browserHistory from './browserHistory' 29 | export hashHistory from './hashHistory' 30 | export createMemoryHistory from './createMemoryHistory' 31 | -------------------------------------------------------------------------------- /modules/PropTypes.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 | pushState: func.isRequired, 13 | replaceState: func.isRequired, 14 | go: func.isRequired 15 | }) 16 | 17 | export const location = shape({ 18 | pathname: string.isRequired, 19 | search: string.isRequired, 20 | state: object, 21 | action: string.isRequired, 22 | key: string 23 | }) 24 | 25 | export const component = oneOfType([ func, string ]) 26 | export const components = oneOfType([ component, object ]) 27 | export const route = oneOfType([ object, element ]) 28 | export const routes = oneOfType([ route, arrayOf(route) ]) 29 | 30 | export default { 31 | falsy, 32 | history, 33 | location, 34 | component, 35 | components, 36 | route 37 | } 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/deprecateObjectProperties.js: -------------------------------------------------------------------------------- 1 | /*eslint no-empty: 0*/ 2 | import warning from './routerWarning' 3 | 4 | let useMembrane = false 5 | 6 | if (__DEV__) { 7 | try { 8 | if (Object.defineProperty({}, 'x', { get() { return true } }).x) { 9 | useMembrane = true 10 | } 11 | } catch(e) { } 12 | } 13 | 14 | // wraps an object in a membrane to warn about deprecated property access 15 | export default function deprecateObjectProperties(object, message) { 16 | if (!useMembrane) 17 | return object 18 | 19 | const membrane = {} 20 | 21 | for (let prop in object) { 22 | if (typeof object[prop] === 'function') { 23 | membrane[prop] = function () { 24 | warning(false, message) 25 | return object[prop].apply(object, arguments) 26 | } 27 | } else { 28 | Object.defineProperty(membrane, prop, { 29 | configurable: false, 30 | enumerable: false, 31 | get() { 32 | warning(false, message) 33 | return object[prop] 34 | } 35 | }) 36 | } 37 | } 38 | 39 | return membrane 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 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React 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 React.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/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/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 | -------------------------------------------------------------------------------- /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 | 9 | // skipping to remove warnings, and we don't intent to update this mixin 10 | // keeping tests here just in-case 11 | describe('v1 History Mixin', function () { 12 | 13 | let node 14 | beforeEach(function () { 15 | node = document.createElement('div') 16 | }) 17 | 18 | afterEach(function () { 19 | unmountComponentAtNode(node) 20 | }) 21 | 22 | it('assigns the history to the component instance', function (done) { 23 | const history = createHistory('/') 24 | 25 | const Component = React.createClass({ 26 | mixins: [ History ], 27 | componentWillMount() { 28 | expect(this.history).toExist() 29 | }, 30 | render() { return null } 31 | }) 32 | 33 | render(( 34 | 35 | 36 | 37 | ), node, done) 38 | }) 39 | 40 | }) 41 | -------------------------------------------------------------------------------- /examples/huge-apps/routes/Course/components/Course.js: -------------------------------------------------------------------------------- 1 | /*globals COURSES:true */ 2 | import React 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 React.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 | -------------------------------------------------------------------------------- /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 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 | 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 './PropTypes' 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 './PropTypes' 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 | -------------------------------------------------------------------------------- /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 './PropTypes' 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 auth from '../utils/auth.js' 3 | 4 | const Login = React.createClass({ 5 | 6 | contextTypes: { 7 | router: React.PropTypes.object 8 | }, 9 | 10 | getInitialState() { 11 | return { 12 | error: false 13 | } 14 | }, 15 | 16 | handleSubmit(event) { 17 | event.preventDefault() 18 | 19 | const email = this.refs.email.value 20 | const pass = this.refs.pass.value 21 | 22 | auth.login(email, pass, (loggedIn) => { 23 | if (!loggedIn) 24 | return this.setState({ error: true }) 25 | 26 | const { location } = this.props 27 | 28 | if (location.state && location.state.nextPathname) { 29 | this.context.router.replace(location.state.nextPathname) 30 | } else { 31 | this.context.router.replace('/') 32 | } 33 | }) 34 | }, 35 | 36 | render() { 37 | return ( 38 |
39 | 40 | (hint: password1)
41 | 42 | {this.state.error && ( 43 |

Bad login information

44 | )} 45 |
46 | ) 47 | } 48 | 49 | }) 50 | 51 | export default Login 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | # This is a workaround for a nasty npm bug. :'( 48 | # First, we need to uninstall the history package so 49 | # it's not included in the react-router npm package. 50 | # https://github.com/reactjs/react-router/issues/2195 51 | # https://github.com/npm/npm/issues/9894 52 | rm -rf node_modules/history 53 | 54 | npm publish 55 | 56 | # And then re-install it after we publish. 57 | npm install history 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 makes any imports from the top-level CommonJS bundle. 4 | 5 | There are two options for minimizing client bundle size by excluding unused modules. 6 | 7 | 8 | ## Import from `react-router/lib` 9 | 10 | Bindings exported from `react-router` are also available in `react-router/lib`. When using CommonJS models, you can import directly from `react-router/lib` to avoid pulling in unused modules. 11 | 12 | Assuming you are transpiling ES2015 modules into CommonJS modules, instead of 13 | 14 | ```js 15 | import { Link, Route, Router } from 'react-router' 16 | ``` 17 | 18 | use 19 | 20 | ```js 21 | import Link from 'react-router/lib/Link' 22 | import Route from 'react-router/lib/Route' 23 | import Router from 'react-router/lib/Router' 24 | ``` 25 | 26 | 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. 27 | 28 | 29 | ## Use a Bundler with ES2015 Module Support 30 | 31 | React Router offers a ES2015 module build under `es6/` and defines a `jsnext:main` entry point. If you are using a bundler that supports ES2015 modules and tree-shaking such as webpack 2 or Rollup, you can directly import from `react-router`, as long as you are correctly resolving to the ES2015 module build. 32 | -------------------------------------------------------------------------------- /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 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 React.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 from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link, Redirect } from 'react-router' 4 | 5 | class App extends React.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 React.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 React.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/__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 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 React.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 6 | issue. 7 | 8 | - [Think You Found a Bug?](#bug) 9 | - [Proposing New or Changed API?](#api) 10 | - [Issue Not Getting Attention?](#attention) 11 | - [Making a Pull Request?](#pr) 12 | - [Development](#development) 13 | - [Hacking](#hacking) 14 | 15 | 16 | ## Think You Found a Bug? 17 | 18 | Please provide a test case of some sort. Best is a pull request with a 19 | failing test. Next is a link to codepen/jsbin or repository that 20 | illustrates the bug. Finally, some copy/pastable code is acceptable. 21 | 22 | If you don't provide a test case, the issue will be closed. 23 | 24 | 25 | ## Proposing New or Changed API? 26 | 27 | Please provide thoughtful comments and some sample code. Proposals 28 | without substance will be closed. 29 | 30 | 31 | ## Issue Not Getting Attention? 32 | 33 | If you need a bug fixed and nobody is fixing it, it is your 34 | responsibility to fix it. Issues with no activity for 30 days may be 35 | closed. 36 | 37 | 38 | ## Making a Pull Request? 39 | 40 | ### Tests 41 | 42 | All commits that fix bugs or add features need a test. 43 | 44 | ``Do not merge code without tests.`` 45 | 46 | ### Changelog 47 | 48 | All commits that change or add to the API must be done in a pull request 49 | that also: 50 | 51 | - Add an entry to `CHANGES.md` with clear steps for updating code for 52 | changed or removed API. 53 | - Updates examples 54 | - Updates the docs 55 | 56 | ## Development 57 | 58 | - `npm test` will fire up a karma test runner and watch for changes 59 | - `npm start` fires up a webpack dev server that will watch 60 | for changes and build the examples 61 | 62 | ## Hacking 63 | 64 | The best way to hack on the router is to symlink it into your project 65 | using [`npm link`](https://docs.npmjs.com/cli/link). Then, use `npm run watch` 66 | to automatically watch the `modules` directory and output a new `build` 67 | every time something changes. 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/passing-props-to-children/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link } from 'react-router' 4 | import './app.css' 5 | 6 | const App = React.createClass({ 7 | contextTypes: { 8 | router: React.PropTypes.object.isRequired 9 | }, 10 | 11 | getInitialState() { 12 | return { 13 | tacos: [ 14 | { name: 'duck confit' }, 15 | { name: 'carne asada' }, 16 | { name: 'shrimp' } 17 | ] 18 | } 19 | }, 20 | 21 | addTaco() { 22 | let name = prompt('taco name?') 23 | 24 | this.setState({ 25 | tacos: this.state.tacos.concat({ name }) 26 | }) 27 | }, 28 | 29 | handleRemoveTaco(removedTaco) { 30 | this.setState({ 31 | tacos: this.state.tacos.filter(function (taco) { 32 | return taco.name != removedTaco 33 | }) 34 | }) 35 | 36 | this.context.router.push('/') 37 | }, 38 | 39 | render() { 40 | let links = this.state.tacos.map(function (taco, i) { 41 | return ( 42 |
  • 43 | {taco.name} 44 |
  • 45 | ) 46 | }) 47 | return ( 48 |
    49 | 50 |
      51 | {links} 52 |
    53 |
    54 | {this.props.children && React.cloneElement(this.props.children, { 55 | onRemoveTaco: this.handleRemoveTaco 56 | })} 57 |
    58 |
    59 | ) 60 | } 61 | }) 62 | 63 | const Taco = React.createClass({ 64 | remove() { 65 | this.props.onRemoveTaco(this.props.params.name) 66 | }, 67 | 68 | render() { 69 | return ( 70 |
    71 |

    {this.props.params.name}

    72 | 73 |
    74 | ) 75 | } 76 | }) 77 | 78 | render(( 79 | 80 | 81 | 82 | 83 | 84 | ), document.getElementById('example')) 85 | -------------------------------------------------------------------------------- /examples/confirming-navigation/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory, Router, Route, Link } 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 = React.createClass({ 26 | contextTypes: { 27 | router: React.PropTypes.object.isRequired 28 | }, 29 | 30 | componentWillMount() { 31 | this.context.router.setRouteLeaveHook( 32 | this.props.route, 33 | this.routerWillLeave 34 | ) 35 | }, 36 | 37 | getInitialState() { 38 | return { 39 | textValue: 'ohai' 40 | } 41 | }, 42 | 43 | routerWillLeave() { 44 | if (this.state.textValue) 45 | return 'You have unsaved information, are you sure you want to leave this page?' 46 | }, 47 | 48 | handleChange(event) { 49 | this.setState({ 50 | textValue: event.target.value 51 | }) 52 | }, 53 | 54 | handleSubmit(event) { 55 | event.preventDefault() 56 | 57 | this.setState({ 58 | textValue: '' 59 | }, () => { 60 | this.context.router.push('/') 61 | }) 62 | }, 63 | 64 | render() { 65 | return ( 66 |
    67 |
    68 |

    Click the dashboard link with text in the input.

    69 | 70 | 71 |
    72 |
    73 | ) 74 | } 75 | }) 76 | 77 | render(( 78 | 79 | 80 | 81 | 82 | 83 | 84 | ), document.getElementById('example')) 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 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 React.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 | 96 | -------------------------------------------------------------------------------- /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 './PropTypes' 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 | -------------------------------------------------------------------------------- /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 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 React.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 React.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 React.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 React.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ### `this.context.router` is `undefined` 4 | 5 | You need to add `router` to your component's `contextTypes` to make the router object available to you. 6 | 7 | ```js 8 | contextTypes: { 9 | router: React.PropTypes.object.isRequired 10 | } 11 | ``` 12 | 13 | 14 | ### Getting the previous location 15 | 16 | ```js 17 | 18 | {/* ... other routes */} 19 | 20 | 21 | const App = React.createClass({ 22 | getInitialState() { 23 | return { showBackButton: false } 24 | }, 25 | 26 | componentWillReceiveProps(nextProps) { 27 | const routeChanged = nextProps.location !== this.props.location 28 | this.setState({ showBackButton: routeChanged }) 29 | } 30 | }) 31 | ``` 32 | 33 | ### Component won't render 34 | 35 | Route matching happens in the order they are defined (think `if/elseif` 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: 36 | 37 | ```js 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 | `About` is now reachable: 45 | 46 | ```js 47 | 48 | 49 | 50 | 51 | ``` 52 | 53 | 54 | ### "Required prop was not specified" on route components 55 | 56 | 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). 57 | 58 | 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. 59 | 60 | 61 | ### `