Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

* [Utilities](#utilities)
* [useRoutes](#useroutescreatehistory)
* [match](#matchlocation-cb)
* [match](#match-routes-location-options--cb)
* [createRoutes](#createroutesroutes)
* [PropTypes](#proptypes)

Expand Down Expand Up @@ -677,14 +677,16 @@ Returns a new `createHistory` function that may be used to create history object
- isActive(pathname, query, indexOnly=false)


## `match(location, cb)`
## `match({ routes, location, ...options }, cb)`

This function is to be used for server-side rendering. It matches a set of routes to a location, without rendering, and calls a `callback(error, redirectLocation, renderProps)` when it's done.

The additional `options` are used to create the history. You can specify `basename` to control the base name for URLs, as well as the pair of `parseQueryString` and `stringifyQuery` to control query string parsing and serializing.

The three arguments to the callback function you pass to `match` are:
* `error`: A Javascript `Error` object if an error occurred, `undefined` otherwise.
* `redirectLocation`: A [Location](/docs/Glossary.md#location) object if the route is a redirect, `undefined` otherwise.
* `renderProps`: The props you should pass to the routing context if the route matched, `undefined` otherwise.
- `error`: A Javascript `Error` object if an error occurred, `undefined` otherwise.
- `redirectLocation`: A [Location](/docs/Glossary.md#location) object if the route is a redirect, `undefined` otherwise.
- `renderProps`: The props you should pass to the routing context if the route matched, `undefined` otherwise.

If all three parameters are `undefined`, this means that there was no route found matching the given location.

Expand Down
2 changes: 1 addition & 1 deletion docs/Glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ A *router* is a [`history`](http://rackt.github.io/history) object (akin to `win
There are two primary interfaces for computing a router's next [state](#routerstate):

- `history.listen` is to be used in stateful environments (such as web browsers) that need to update the UI over a period of time. This method immediately invokes its `listener` argument once and returns a function that must be called to stop listening for changes
- `history.match` is a pure asynchronous function that does not update the history's internal state. This makes it ideal for server-side environments where many requests must be handled concurrently
- `history.match` is a function that does not update the history's internal state. This makes it ideal for server-side environments where many requests must be handled concurrently

## RouterListener

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/advanced/ServerRendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Server rendering is a bit different than in a client because you'll want to:

To facilitate these needs, you drop one level lower than the [`<Router>`](/docs/API.md#Router) API with:

- [`match`](https://github.com/rackt/react-router/blob/master/docs/API.md#matchlocation-cb) to match the routes to a location without rendering
- [`match`](/docs/API.md#match-routes-location-options--cb) to match the routes to a location without rendering
- `RouterContext` for synchronous rendering of route components

It looks something like this with an imaginary JavaScript server:
Expand All @@ -21,7 +21,7 @@ import routes from './routes'
serve((req, res) => {
// Note that req.url here should be the full URL path from
// the original request, including the query string.
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
match({ routes, path: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
Expand Down
10 changes: 8 additions & 2 deletions modules/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,15 @@ const Router = React.createClass({
this._unlisten = this.history.listen((error, state) => {
if (error) {
this.handleError(error)
} else {
this.setState(state, this.props.onUpdate)
return
}

if (!state) {
// No match.
return
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... don't do anything on the <Router> if there wasn't a match.

}

this.setState(state, this.props.onUpdate)
})
},

Expand Down
10 changes: 9 additions & 1 deletion modules/__tests__/matchRoutes-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@ import matchRoutes from '../matchRoutes'
import Route from '../Route'
import IndexRoute from '../IndexRoute'

function createLocation(path) {
const history = createMemoryHistory(path)
let result
history.listen(location => {
result = location
})
return result
}

describe('matchRoutes', function () {
let routes
let
RootRoute, UsersRoute, UsersIndexRoute, UserRoute, PostRoute, FilesRoute,
AboutRoute, TeamRoute, ProfileRoute, GreedyRoute, OptionalRoute,
OptionalRouteChild, CatchAllRoute
let createLocation = createMemoryHistory().createLocation

beforeEach(function () {
/*
Expand Down
28 changes: 28 additions & 0 deletions modules/__tests__/serverRendering-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,32 @@ describe('server rendering', function () {
})
})

it('works with location descriptor object', function (done) {
const location = { pathname: '/dashboard', query: { the: 'query' } }

match({ routes, location }, function (error, redirectLocation, renderProps) {
const string = renderToString(
<RouterContext {...renderProps} />
)

expect(string).toMatch(/The Dashboard/)
expect(renderProps.location.search).toEqual('?the=query')

done()
})
})

it('only fires the callback once', function () {
const callback = expect.createSpy().andCall(
function (error, redirectLocation, renderProps) {
if (renderProps.location.pathname === '/dashboard') {
renderProps.history.push('/about')
}
}
)

match({ routes, location: '/dashboard' }, callback)
expect(callback.calls.length).toEqual(1)
})

})
44 changes: 33 additions & 11 deletions modules/match.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import invariant from 'invariant'
import { REPLACE } from 'history/lib/Actions'
import createMemoryHistory from 'history/lib/createMemoryHistory'
import useBasename from 'history/lib/useBasename'
import { createRoutes } from './RouteUtils'
Expand All @@ -18,9 +19,7 @@ const createHistory = useRoutes(useBasename(createMemoryHistory))
function match({
routes,
location,
parseQueryString,
stringifyQuery,
basename
...options
}, callback) {
invariant(
location,
Expand All @@ -29,17 +28,40 @@ function match({

const history = createHistory({
routes: createRoutes(routes),
parseQueryString,
stringifyQuery,
basename
...options
})
history.push(location)

// Allow match({ location: '/the/path', ... })
if (typeof location === 'string')
location = history.createLocation(location)
let fired = false

history.match(location, function (error, redirectLocation, nextState) {
callback(error, redirectLocation, nextState && { ...nextState, history })
// We don't have to clean up listeners here, because this is just a memory
// history.
history.listen((error, state) => {
// This isn't fully stateless - if the user redirects as a consequence of
// rendering, this ensures that we won't fire the callback twice.
if (fired) {
return
}
fired = true

if (error) {
callback(error)
return
}

if (!state) {
// No match.
callback()
return
}

if (state.location.action === REPLACE) {
// This was a redirect.
callback(null, state.location)
return
}

callback(null, null, { ...state, history })
})
}

Expand Down
13 changes: 4 additions & 9 deletions modules/useRoutes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import warning from 'warning'
import { REPLACE } from 'history/lib/Actions'
import useQueries from 'history/lib/useQueries'
import computeChangedRoutes from './computeChangedRoutes'
import { runEnterHooks, runLeaveHooks } from './TransitionUtils'
Expand Down Expand Up @@ -36,12 +35,6 @@ function useRoutes(createHistory) {
return _isActive(pathname, query, indexOnly, state.location, state.routes, state.params)
}

function createLocationFromRedirectInfo({ pathname, query, state }) {
return history.createLocation(
history.createPath(pathname, query), state, REPLACE
)
}

let partialNextState

function match(location, callback) {
Expand Down Expand Up @@ -70,7 +63,7 @@ function useRoutes(createHistory) {
if (error) {
callback(error)
} else if (redirectInfo) {
callback(null, createLocationFromRedirectInfo(redirectInfo))
callback(null, redirectInfo)
} else {
// TODO: Fetch components after state is updated.
getComponents(nextState, function (error, components) {
Expand Down Expand Up @@ -229,7 +222,7 @@ function useRoutes(createHistory) {
if (error) {
listener(error)
} else if (redirectLocation) {
history.transitionTo(redirectLocation)
history.replace(redirectLocation)
} else if (nextState) {
listener(null, nextState)
} else {
Expand All @@ -238,6 +231,8 @@ function useRoutes(createHistory) {
'Location "%s" did not match any routes',
location.pathname + location.search + location.hash
)

listener()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always fire the listener, but...

}
})
}
Expand Down