Skip to content

Commit 3a162a1

Browse files
committed
Merge pull request #3430 from gaearon/fix-redux-2
Make <Link> work with static containers (take two)
2 parents da81910 + 1d0ed5e commit 3a162a1

File tree

4 files changed

+170
-3
lines changed

4 files changed

+170
-3
lines changed

modules/ContextUtils.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React, { PropTypes } from 'react'
2+
3+
// Works around issues with context updates failing to propagate.
4+
// https://github.com/facebook/react/issues/2517
5+
// https://github.com/reactjs/react-router/issues/470
6+
7+
export function createContextProvider(name, contextType = PropTypes.any) {
8+
const ContextProvider = React.createClass({
9+
propTypes: {
10+
children: PropTypes.node.isRequired
11+
},
12+
13+
contextTypes: {
14+
[name]: contextType
15+
},
16+
17+
childContextTypes: {
18+
[name]: contextType
19+
},
20+
21+
getChildContext() {
22+
return {
23+
[name]: {
24+
...this.context[name],
25+
subscribe: this.subscribe,
26+
eventIndex: this.eventIndex
27+
}
28+
}
29+
},
30+
31+
componentWillMount() {
32+
this.eventIndex = 0
33+
this.listeners = []
34+
},
35+
36+
componentWillReceiveProps() {
37+
this.eventIndex++
38+
},
39+
40+
componentDidUpdate() {
41+
this.listeners.forEach(listener => listener(this.eventIndex))
42+
},
43+
44+
subscribe(listener) {
45+
// No need to immediately call listener here.
46+
this.listeners.push(listener)
47+
48+
return () => {
49+
this.listeners = this.listeners.filter(item => item !== listener)
50+
}
51+
},
52+
53+
render() {
54+
return this.props.children
55+
}
56+
})
57+
58+
return ContextProvider
59+
}
60+
61+
export function connectToContext(WrappedComponent, name, contextType = PropTypes.any) {
62+
const ContextSubscriber = React.createClass({
63+
contextTypes: {
64+
[name]: contextType
65+
},
66+
67+
getInitialState() {
68+
if (!this.context[name]) {
69+
return {}
70+
}
71+
72+
return {
73+
lastRenderedEventIndex: this.context[name].eventIndex
74+
}
75+
},
76+
77+
componentDidMount() {
78+
if (!this.context[name]) {
79+
return
80+
}
81+
82+
this.unsubscribe = this.context[name].listen(eventIndex => {
83+
if (eventIndex !== this.state.lastRenderedEventIndex) {
84+
this.setState({ lastRenderedEventIndex: eventIndex })
85+
}
86+
})
87+
},
88+
89+
componentWillReceiveProps() {
90+
if (!this.context[name]) {
91+
return
92+
}
93+
94+
this.setState({
95+
lastRenderedEventIndex: this.context[name].eventIndex
96+
})
97+
},
98+
99+
componentWillUnmount() {
100+
if (!this.unsubscribe) {
101+
return
102+
}
103+
104+
this.unsubscribe()
105+
this.unsubscribe = null
106+
},
107+
108+
render() {
109+
return <WrappedComponent {...this.props} />
110+
}
111+
})
112+
113+
return ContextSubscriber
114+
}

modules/Link.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react'
22
import { routerShape } from './PropTypes'
3+
import { connectToContext } from './ContextUtils'
34

45
const { bool, object, string, func, oneOfType } = React.PropTypes
56

@@ -121,4 +122,4 @@ const Link = React.createClass({
121122

122123
})
123124

124-
export default Link
125+
export default connectToContext(Link, 'router', object)

modules/RouterContext.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import invariant from 'invariant'
22
import React from 'react'
33

44
import getRouteParams from './getRouteParams'
5+
import { createContextProvider } from './ContextUtils'
56
import { isReactChildren } from './RouteUtils'
67

78
const { array, func, object } = React.PropTypes
9+
const RouterContextProvider = createContextProvider('router', object.isRequired)
810

911
/**
1012
* A <RouterContext> renders the component tree for a given router state
@@ -88,12 +90,21 @@ const RouterContext = React.createClass({
8890
}, element)
8991
}
9092

93+
const isEmpty = element === null || element === false
9194
invariant(
92-
element === null || element === false || React.isValidElement(element),
95+
isEmpty || React.isValidElement(element),
9396
'The root route must render a single element'
9497
)
9598

96-
return element
99+
if (isEmpty) {
100+
return element
101+
}
102+
103+
return (
104+
<RouterContextProvider>
105+
{element}
106+
</RouterContextProvider>
107+
)
97108
}
98109

99110
})

modules/__tests__/Link-test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,47 @@ describe('A <Link>', function () {
314314
</Router>
315315
), node, execNextStep)
316316
})
317+
318+
it('changes active state inside static containers', function (done) {
319+
class LinkWrapper extends Component {
320+
shouldComponentUpdate() {
321+
return false
322+
}
323+
324+
render() {
325+
return (
326+
<div>
327+
<Link to="/hello" activeClassName="active">Link</Link>
328+
{this.props.children}
329+
</div>
330+
)
331+
}
332+
}
333+
334+
let a
335+
const history = createHistory('/goodbye')
336+
const steps = [
337+
function () {
338+
a = node.querySelector('a')
339+
expect(a.className).toEqual('')
340+
history.push('/hello')
341+
},
342+
function () {
343+
expect(a.className).toEqual('active')
344+
}
345+
]
346+
347+
const execNextStep = execSteps(steps, done)
348+
349+
render((
350+
<Router history={history} onUpdate={execNextStep}>
351+
<Route path="/" component={LinkWrapper}>
352+
<Route path="goodbye" component={Goodbye} />
353+
<Route path="hello" component={Hello} />
354+
</Route>
355+
</Router>
356+
), node, execNextStep)
357+
})
317358
})
318359

319360
describe('when clicked', function () {

0 commit comments

Comments
 (0)