forked from remix-run/react-router
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit adds two functions: 1. Router.renderRoutesToStaticMarkup(routes, path, callback) 2. Router.renderRoutesToString(routes, path, callback) These methods are the equivalents to React's renderComponentTo* methods, except they are designed specially to work with <Routes> components. This commit obsoletes remix-run#181. Many thanks to @karlmikko and others in that thread for getting the conversation going around how this should all work.
- Loading branch information
Showing
4 changed files
with
264 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
var ReactDescriptor = require('react/lib/ReactDescriptor'); | ||
var ReactInstanceHandles = require('react/lib/ReactInstanceHandles'); | ||
var ReactMarkupChecksum = require('react/lib/ReactMarkupChecksum'); | ||
var ReactServerRenderingTransaction = require('react/lib/ReactServerRenderingTransaction'); | ||
|
||
var cloneWithProps = require('react/lib/cloneWithProps'); | ||
var copyProperties = require('react/lib/copyProperties'); | ||
var instantiateReactComponent = require('react/lib/instantiateReactComponent'); | ||
var invariant = require('react/lib/invariant'); | ||
|
||
function cloneRoutesForServerRendering(routes) { | ||
return cloneWithProps(routes, { | ||
location: 'none', | ||
scrollBehavior: 'none' | ||
}); | ||
} | ||
|
||
function mergeStateIntoInitialProps(state, props) { | ||
copyProperties(props, { | ||
initialPath: state.path, | ||
initialMatches: state.matches, | ||
initialActiveRoutes: state.activeRoutes, | ||
initialActiveParams: state.activeParams, | ||
initialActiveQuery: state.activeQuery | ||
}); | ||
} | ||
|
||
/** | ||
* Renders a <Routes> component to a string of HTML at the given URL | ||
* path and calls callback(error, abortReason, html) when finished. | ||
* | ||
* If there was an error during the transition, it is passed to the | ||
* callback. Otherwise, if the transition was aborted for some reason, | ||
* it is given in the abortReason argument (with the exception of | ||
* internal redirects which are transparently handled for you). | ||
* | ||
* TODO: <NotFoundRoute> should be handled specially so servers know | ||
* to use a 404 status code. | ||
*/ | ||
function renderRoutesToString(routes, path, callback) { | ||
invariant( | ||
ReactDescriptor.isValidDescriptor(routes), | ||
'You must pass a valid ReactComponent to renderRoutesToString' | ||
); | ||
|
||
var component = instantiateReactComponent( | ||
cloneRoutesForServerRendering(routes) | ||
); | ||
|
||
component.dispatch(path, function (error, abortReason, nextState) { | ||
if (error || abortReason) | ||
return callback(error, abortReason); | ||
|
||
mergeStateIntoInitialProps(nextState, component.props); | ||
|
||
var transaction; | ||
try { | ||
var id = ReactInstanceHandles.createReactRootID(); | ||
transaction = ReactServerRenderingTransaction.getPooled(false); | ||
|
||
transaction.perform(function() { | ||
var markup = component.mountComponent(id, transaction, 0); | ||
callback(null, null, ReactMarkupChecksum.addChecksumToMarkup(markup)); | ||
}, null); | ||
} finally { | ||
ReactServerRenderingTransaction.release(transaction); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Renders a <Routes> component to static markup at the given URL | ||
* path and calls callback(error, abortReason, markup) when finished. | ||
*/ | ||
function renderRoutesToStaticMarkup(routes, path, callback) { | ||
invariant( | ||
ReactDescriptor.isValidDescriptor(routes), | ||
'You must pass a valid ReactComponent to renderRoutesToStaticMarkup' | ||
); | ||
|
||
var component = instantiateReactComponent( | ||
cloneRoutesForServerRendering(routes) | ||
); | ||
|
||
component.dispatch(path, function (error, abortReason, nextState) { | ||
if (error || abortReason) | ||
return callback(error, abortReason); | ||
|
||
mergeStateIntoInitialProps(nextState, component.props); | ||
|
||
var transaction; | ||
try { | ||
var id = ReactInstanceHandles.createReactRootID(); | ||
transaction = ReactServerRenderingTransaction.getPooled(false); | ||
|
||
transaction.perform(function() { | ||
callback(null, null, component.mountComponent(id, transaction, 0)); | ||
}, null); | ||
} finally { | ||
ReactServerRenderingTransaction.release(transaction); | ||
} | ||
}); | ||
} | ||
|
||
module.exports = { | ||
renderRoutesToString: renderRoutesToString, | ||
renderRoutesToStaticMarkup: renderRoutesToStaticMarkup | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
var assert = require('assert'); | ||
var expect = require('expect'); | ||
var React = require('react'); | ||
var Link = require('../../components/Link'); | ||
var Routes = require('../../components/Routes'); | ||
var Route = require('../../components/Route'); | ||
var ServerRendering = require('../ServerRendering'); | ||
|
||
describe('ServerRendering', function () { | ||
|
||
describe('renderRoutesToMarkup', function () { | ||
describe('a very simple case', function () { | ||
var Home = React.createClass({ | ||
render: function () { | ||
return React.DOM.b(null, 'Hello ' + this.props.params.username + '!'); | ||
} | ||
}); | ||
|
||
var output; | ||
beforeEach(function (done) { | ||
var routes = Routes(null, | ||
Route({ path: '/home/:username', handler: Home }) | ||
); | ||
|
||
ServerRendering.renderRoutesToStaticMarkup(routes, '/home/mjackson', function (error, abortReason, markup) { | ||
assert(error == null); | ||
assert(abortReason == null); | ||
output = markup; | ||
done(); | ||
}); | ||
}); | ||
|
||
it('has the correct output', function () { | ||
expect(output).toMatch(/^<b data-reactid="[\.a-z0-9]+">Hello mjackson!<\/b>$/); | ||
}); | ||
}); | ||
|
||
describe('an embedded <Link> to the current route', function () { | ||
var Home = React.createClass({ | ||
render: function () { | ||
return Link({ to: 'home', params: { username: 'mjackson' } }, 'Hello ' + this.props.params.username + '!'); | ||
} | ||
}); | ||
|
||
var output; | ||
beforeEach(function (done) { | ||
var routes = Routes(null, | ||
Route({ name: 'home', path: '/home/:username', handler: Home }) | ||
); | ||
|
||
ServerRendering.renderRoutesToStaticMarkup(routes, '/home/mjackson', function (error, abortReason, markup) { | ||
assert(error == null); | ||
assert(abortReason == null); | ||
output = markup; | ||
done(); | ||
}); | ||
}); | ||
|
||
it('has the correct output', function () { | ||
expect(output).toMatch(/^<a href="\/home\/mjackson" class="active" data-reactid="[\.a-z0-9]+">Hello mjackson!<\/a>$/); | ||
}); | ||
}); | ||
|
||
describe('when the transition is aborted', function () { | ||
var Home = React.createClass({ | ||
statics: { | ||
willTransitionTo: function (transition) { | ||
transition.abort({ status: 403 }); | ||
} | ||
}, | ||
render: function () { | ||
return null; | ||
} | ||
}); | ||
|
||
var reason; | ||
beforeEach(function (done) { | ||
var routes = Routes(null, | ||
Route({ name: 'home', path: '/home/:username', handler: Home }) | ||
); | ||
|
||
ServerRendering.renderRoutesToStaticMarkup(routes, '/home/mjackson', function (error, abortReason) { | ||
assert(error == null); | ||
reason = abortReason; | ||
done(); | ||
}); | ||
}); | ||
|
||
it('gives the reason in the callback', function () { | ||
assert(reason); | ||
expect(reason.status).toEqual(403); | ||
}); | ||
}); | ||
|
||
describe('when there is an error performing the transition', function () { | ||
var Home = React.createClass({ | ||
statics: { | ||
willTransitionTo: function (transition) { | ||
throw 'boom!'; | ||
} | ||
}, | ||
render: function () { | ||
return null; | ||
} | ||
}); | ||
|
||
var error; | ||
beforeEach(function (done) { | ||
var routes = Routes(null, | ||
Route({ name: 'home', path: '/home/:username', handler: Home }) | ||
); | ||
|
||
ServerRendering.renderRoutesToStaticMarkup(routes, '/home/mjackson', function (e, abortReason) { | ||
assert(abortReason == null); | ||
error = e; | ||
done(); | ||
}); | ||
}); | ||
|
||
it('gives the reason in the callback', function () { | ||
expect(error).toEqual('boom!'); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('renderRoutesToString', function () { | ||
var Home = React.createClass({ | ||
render: function () { | ||
return React.DOM.b(null, 'Hello ' + this.props.params.username + '!'); | ||
} | ||
}); | ||
|
||
var output; | ||
beforeEach(function (done) { | ||
var routes = Routes(null, | ||
Route({ path: '/home/:username', handler: Home }) | ||
); | ||
|
||
ServerRendering.renderRoutesToString(routes, '/home/mjackson', function (error, abortReason, string) { | ||
assert(error == null); | ||
assert(abortReason == null); | ||
output = string; | ||
done(); | ||
}); | ||
}); | ||
|
||
it('has the correct output', function () { | ||
expect(output).toMatch(/^<b data-reactid="[\.a-z0-9]+" data-react-checksum="\d+">Hello mjackson!<\/b>$/); | ||
}); | ||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters