Skip to content

Commit f02b919

Browse files
committed
add <Miss> support for <ServerRouter> 😓
So basically you do a two pass render, JavaScript will explain it better than English so here’s the gist of it: ```jsx const context = createServerRenderContext() let html = renderToString(<ServerRouter context={context}/>) const result = context.getResult() if (result.missed) { // do it again html = renderToString(<ServerRouter context={context}/>) } res.send(result.missed ? 404 : 200, html) ``` Hopefully one day we’ll have `componentDidServerRender` and we’ll be able to clean this up a bit.
1 parent ab6cc33 commit f02b919

File tree

8 files changed

+173
-70
lines changed

8 files changed

+173
-70
lines changed

‎modules/Match.js‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ class RegisterMatch extends React.Component {
99
}
1010

1111
static contextTypes = {
12-
match: PropTypes.object
12+
match: PropTypes.object,
13+
serverRouter: PropTypes.object
1314
}
1415

15-
componentDidMount() {
16+
registerMatch() {
1617
const { match:matchContext } = this.context
1718
const { match } = this.props
1819

@@ -21,6 +22,18 @@ class RegisterMatch extends React.Component {
2122
}
2223
}
2324

25+
componentWillMount() {
26+
if (this.context.serverRouter) {
27+
this.registerMatch()
28+
}
29+
}
30+
31+
componentDidMount() {
32+
if (!this.context.serverRouter) {
33+
this.registerMatch()
34+
}
35+
}
36+
2437
componentDidUpdate(prevProps) {
2538
const { match } = this.context
2639

‎modules/MatchProvider.js‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,25 @@ class MatchProvider extends React.Component {
1313
match: matchContextType.isRequired
1414
}
1515

16+
static contextTypes = {
17+
serverRouter: PropTypes.object
18+
}
19+
1620
constructor(props) {
1721
super(props)
1822
this.parent = props.match
1923
// React doesn't support a parent calling `setState` from an descendant's
2024
// componentWillMount, so we use an instance property to track matches
25+
// **IMPORTANT** we must mutate matches, never reassign, in order for
26+
// server rendering to work
2127
this.matches = []
2228
this.subscribers = []
2329
this.hasMatches = null // use null for initial value
30+
this.serverRouterIndex = null
2431
}
2532

2633
addMatch = match => {
27-
this.matches = this.matches.concat([match])
34+
this.matches.push(match)
2835
}
2936

3037
removeMatch = match => {
@@ -36,17 +43,15 @@ class MatchProvider extends React.Component {
3643
match: {
3744
addMatch: this.addMatch,
3845
removeMatch: this.removeMatch,
39-
40-
parent: this.parent,
4146
matches: this.matches,
42-
47+
parent: this.parent,
48+
serverRouterIndex: this.serverRouterIndex,
4349
subscribe: (fn) => {
4450
this.subscribers.push(fn)
4551
return () => {
4652
this.subscribers.splice(this.subscribers.indexOf(fn), 1)
4753
}
4854
}
49-
5055
}
5156
}
5257
}
@@ -55,6 +60,14 @@ class MatchProvider extends React.Component {
5560
this.notifySubscribers()
5661
}
5762

63+
componentWillMount() {
64+
const { serverRouter } = this.context
65+
if (serverRouter) {
66+
this.serverRouterIndex =
67+
serverRouter.registerMatchContext(this.matches)
68+
}
69+
}
70+
5871
componentDidMount() {
5972
this.notifySubscribers()
6073
}

‎modules/Miss.js‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,28 @@ class Miss extends React.Component {
1515
serverRouter: PropTypes.object
1616
}
1717

18-
state = {
19-
noMatchesInContext: false
20-
}
2118

22-
componentWillMount() {
19+
constructor(props, context) {
20+
super(props, context)
21+
2322
// ignore if rendered out of context (probably for unit tests)
24-
if (this.context.match) {
23+
if (context.match && !context.serverRouter) {
2524
this.unsubscribe = this.context.match.subscribe((matchesFound) => {
2625
this.setState({
2726
noMatchesInContext: !matchesFound
2827
})
2928
})
3029
}
30+
31+
if (context.serverRouter) {
32+
context.serverRouter.registerMissPresence(
33+
context.match.serverRouterIndex
34+
)
35+
}
36+
37+
this.state = {
38+
noMatchesInContext: false
39+
}
3140
}
3241

3342
componentWillUnmount() {
@@ -41,7 +50,10 @@ class Miss extends React.Component {
4150
const { noMatchesInContext } = this.state
4251
const { location:locationProp } = this.props
4352
const location = locationProp || this.context.location
44-
if (noMatchesInContext) {
53+
const { serverRouter, match } = this.context
54+
const noMatchesOnServerContext = serverRouter &&
55+
serverRouter.missedAtIndex(match.serverRouterIndex)
56+
if (noMatchesInContext || noMatchesOnServerContext) {
4557
return (
4658
render ? (
4759
render({ location })

‎modules/Redirect.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ class Redirect extends React.Component {
1313

1414
static contextTypes = {
1515
router: routerType,
16-
serverResult: PropTypes.object
16+
serverRouter: PropTypes.object
1717
}
1818

1919
componentWillMount() {
20-
if (this.context.serverResult)
20+
if (this.context.serverRouter)
2121
this.redirect()
2222
}
2323

‎modules/ServerRouter.js‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,31 @@
11
import React, { PropTypes } from 'react'
22
import StaticRouter from './StaticRouter'
3-
import { location as locationType } from './PropTypes'
43

54
class ServerRouter extends React.Component {
65

76
static propTypes = {
8-
result: PropTypes.object,
9-
location: locationType,
7+
context: PropTypes.object.isRequired,
8+
location: PropTypes.string.isRequired,
109
children: PropTypes.oneOfType([
1110
PropTypes.func,
1211
PropTypes.node
1312
])
1413
}
1514

1615
static childContextTypes = {
17-
serverResult: PropTypes.object.isRequired
16+
serverRouter: PropTypes.object.isRequired
1817
}
1918

2019
getChildContext() {
2120
return {
22-
serverResult: this.props.result
21+
serverRouter: this.props.context
2322
}
2423
}
2524

2625
render() {
27-
const { result, ...rest } = this.props
28-
const redirect = (location, state) => {
29-
if (!result.redirect) {
30-
result.redirect = { location, state }
31-
}
26+
const { context, ...rest } = this.props
27+
const redirect = (location) => {
28+
context.setRedirect(location)
3229
}
3330
return (
3431
<StaticRouter

‎modules/StaticRouter.js‎

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class StaticRouter extends React.Component {
1919
blockTransitions: PropTypes.func,
2020
children: PropTypes.oneOfType([ PropTypes.node, PropTypes.func ]),
2121
createHref: PropTypes.func.isRequired,
22-
location: PropTypes.oneOfType([ locationType, PropTypes.string ]).isRequired,
22+
location: PropTypes.oneOfType([ PropTypes.object, PropTypes.string ]).isRequired,
2323
onPush: PropTypes.func.isRequired,
2424
onReplace: PropTypes.func.isRequired,
2525
stringifyQuery: PropTypes.func.isRequired,
@@ -38,31 +38,28 @@ class StaticRouter extends React.Component {
3838
location: locationType.isRequired
3939
}
4040

41+
createLocationForContext(loc) {
42+
const { parseQuery, stringifyQuery } = this.props
43+
return createRouterLocation(loc, parseQuery, stringifyQuery)
44+
}
45+
4146
getChildContext() {
4247
const createHref = (to) => {
4348
const path = createRouterPath(to, this.props.stringifyQuery)
4449
return this.props.createHref(path)
4550
}
4651

47-
const getPathAndState = (loc) => {
48-
const path = createHref(loc)
49-
const state = typeof loc === 'object' ? loc.state : null
50-
return { path, state }
51-
}
52-
5352
const location = this.getLocation()
5453

5554
return {
5655
location,
5756
router: {
5857
createHref,
5958
transitionTo: (loc) => {
60-
const { path, state } = getPathAndState(loc)
61-
this.props.onPush(path, state)
59+
this.props.onPush(this.createLocationForContext(loc))
6260
},
6361
replaceWith: (loc) => {
64-
const { path, state } = getPathAndState(loc)
65-
this.props.onReplace(path, state)
62+
this.props.onReplace(this.createLocationForContext(loc))
6663
},
6764
blockTransitions: (getPromptMessage) => {
6865
this.props.blockTransitions(getPromptMessage)
@@ -86,7 +83,7 @@ class StaticRouter extends React.Component {
8683
{typeof children === 'function' ? (
8784
children({ location, router: this.getChildContext().router })
8885
) : React.Children.count(children) > 1 ? (
89-
// #TODO get rid of all DOM stuff
86+
// TODO: get rid of all DOM stuff
9087
<div>{children}</div>
9188
) : (
9289
children
Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import expect from 'expect'
22
import React from 'react'
33
import ServerRouter from '../ServerRouter'
4+
import createServerRenderContext from '../createServerRenderContext'
45
import Redirect from '../Redirect'
6+
import Match from '../Match'
57
import Miss from '../Miss'
68
import { renderToString } from 'react-dom/server'
79

810
describe('ServerRouter', () => {
911

1012
it('puts redirects on server render result', () => {
11-
const result = {}
13+
const context = createServerRenderContext()
1214

1315
renderToString(
1416
<ServerRouter
15-
result={result}
17+
context={context}
1618
location="/"
1719
>
1820
<Redirect to={{
@@ -21,47 +23,59 @@ describe('ServerRouter', () => {
2123
}}/>
2224
</ServerRouter>
2325
)
24-
expect(result).toEqual({
25-
redirect: {
26-
location: '/somewhere-else',
27-
state: { status: 302 }
28-
}
26+
expect(context.getResult().redirect).toEqual({
27+
pathname: '/somewhere-else',
28+
state: { status: 302 },
29+
query: null,
30+
search: '',
31+
hash: ''
2932
})
3033
})
3134

32-
it.skip('puts misses on server render result', () => {
33-
const result = {}
34-
35-
renderToString(
36-
<ServerRouter
37-
result={result}
38-
location="/anywhere"
39-
>
40-
<Miss render={() => (
41-
<div>hi</div>
35+
it('renders misses on second pass with server render context result', (done) => {
36+
const NO = 'NO'
37+
const YES1 = 'YES1'
38+
const YES2 = 'YES2'
39+
const location = '/nowhere'
40+
const App = () => (
41+
<div>
42+
<Match pattern="/" render={() => (
43+
<div>
44+
<Miss render={() => (
45+
<div>{YES1}</div>
46+
)}/>
47+
<Miss render={() => (
48+
<div>{YES2}</div>
49+
)}/>
50+
</div>
4251
)}/>
43-
</ServerRouter>
52+
<Miss render={() => <div>{NO}</div>}/>
53+
</div>
4454
)
45-
expect(result).toEqual({
46-
misses: [ 0, 2 ]
47-
})
48-
})
4955

50-
it.skip('renders misses from server result', () => {
51-
const result = { misses: [ 0 ] }
56+
const context = createServerRenderContext()
5257

53-
const markup = renderToString(
54-
<ServerRouter
55-
result={result}
56-
location="/anywhere"
57-
>
58-
<Miss render={() => (
59-
<div>test</div>
60-
)}/>
58+
renderToString(
59+
<ServerRouter context={context} location={location}>
60+
<App/>
6161
</ServerRouter>
6262
)
6363

64-
expect(markup).toContain('test')
64+
const result = context.getResult()
65+
expect(result.missed).toBe(true)
66+
67+
if (result.missed) {
68+
const markup = renderToString(
69+
<ServerRouter context={context} location={location}>
70+
<App/>
71+
</ServerRouter>
72+
)
73+
74+
expect(markup).toContain(YES1)
75+
expect(markup).toContain(YES2)
76+
expect(markup).toNotContain(NO)
77+
done()
78+
}
6579
})
6680

6781
})

0 commit comments

Comments
 (0)