Skip to content

Commit dba598b

Browse files
esamattismarkerikson
authored andcommitted
Add connectAdvanced() tests (#1079)
Since the connectAdvanced() is a public api I think it should have some tests. Unfortunately these tests are intentionally failing because I'm not sure how connectAdvanved() should really work. This is just my best guess. For example the behaviour is very different between 5.x and 6 beta. Different tests fail between those and in common failures the errors are different. I have created [redux-render-prop](https://github.com/epeli/redux-render-prop) module which relies on the connectAdvanced() primitive and I noticed that all the important performance related [tests started failing](https://travis-ci.com/epeli/redux-render-prop/jobs/157787740) when I upgraded to 6 beta. Most importantly [`unrelated state updates don't cause render`](https://github.com/epeli/redux-render-prop/blob/aa2aa9b299e5859f7a79bc1c926ed75907f63dcb/__tests__/redux-render-prop.test.tsx#L284-L349). I've added matching test in this PR called `should not render when the returned reference does not change`. I've also observed that in 5.x the state mapping function gets called twice on component mount for which I had to create [a very ugly workaround](https://github.com/epeli/redux-render-prop/blob/aa2aa9b299e5859f7a79bc1c926ed75907f63dcb/src/redux-render-prop.ts#L188-L195) in redux-render-prop to avoid unnecessary rendering. I've added test for that case too (`should map state and render once on mount`). This seems to be fixed in the 6 beta. I hope we can have more stable behaviour for the connectAdvanced() in future.
1 parent 1dc56ca commit dba598b

File tree

2 files changed

+184
-5
lines changed

2 files changed

+184
-5
lines changed

src/components/connectAdvanced.js

+1-5
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,7 @@ export default function connectAdvanced(
203203
store
204204
)
205205

206-
if (pure) {
207-
return this.selectChildElement(derivedProps, forwardedRef)
208-
}
209-
210-
return <FinalWrappedComponent {...derivedProps} ref={forwardedRef} />
206+
return this.selectChildElement(derivedProps, forwardedRef)
211207
}
212208

213209
render() {
+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import React, { Component } from 'react'
2+
import * as rtl from 'react-testing-library'
3+
import { Provider as ProviderMock, connectAdvanced } from '../../src/index.js'
4+
import { createStore } from 'redux'
5+
import 'jest-dom/extend-expect'
6+
7+
describe('React', () => {
8+
describe('connectAdvanced', () => {
9+
it('should map state and render once on mount', () => {
10+
const initialState = {
11+
foo: 'bar'
12+
}
13+
14+
let mapCount = 0
15+
let renderCount = 0
16+
17+
const store = createStore(() => initialState)
18+
19+
function Inner(props) {
20+
renderCount++
21+
return <div data-testid="foo">{JSON.stringify(props)}</div>
22+
}
23+
24+
const Container = connectAdvanced(() => {
25+
return state => {
26+
mapCount++
27+
return state
28+
}
29+
})(Inner)
30+
31+
const tester = rtl.render(
32+
<ProviderMock store={store}>
33+
<Container />
34+
</ProviderMock>
35+
)
36+
37+
expect(tester.getByTestId('foo')).toHaveTextContent('bar')
38+
39+
expect(mapCount).toEqual(1)
40+
expect(renderCount).toEqual(1)
41+
})
42+
43+
it('should render on reference change', () => {
44+
let mapCount = 0
45+
let renderCount = 0
46+
47+
// force new reference on each dispatch
48+
const store = createStore(() => ({
49+
foo: 'bar'
50+
}))
51+
52+
function Inner(props) {
53+
renderCount++
54+
return <div data-testid="foo">{JSON.stringify(props)}</div>
55+
}
56+
57+
const Container = connectAdvanced(() => {
58+
return state => {
59+
mapCount++
60+
return state
61+
}
62+
})(Inner)
63+
64+
rtl.render(
65+
<ProviderMock store={store}>
66+
<Container />
67+
</ProviderMock>
68+
)
69+
70+
store.dispatch({ type: 'NEW_REFERENCE' })
71+
72+
// Should have mapped the state on mount and on the dispatch
73+
expect(mapCount).toEqual(2)
74+
75+
// Should have rendered on mount and after the dispatch bacause the map
76+
// state returned new reference
77+
expect(renderCount).toEqual(2)
78+
})
79+
80+
it('should not render when the returned reference does not change', () => {
81+
const staticReference = {
82+
foo: 'bar'
83+
}
84+
85+
let mapCount = 0
86+
let renderCount = 0
87+
88+
// force new reference on each dispatch
89+
const store = createStore(() => ({
90+
foo: 'bar'
91+
}))
92+
93+
function Inner(props) {
94+
renderCount++
95+
return <div data-testid="foo">{JSON.stringify(props)}</div>
96+
}
97+
98+
const Container = connectAdvanced(() => {
99+
return () => {
100+
mapCount++
101+
// but return static reference
102+
return staticReference
103+
}
104+
})(Inner)
105+
106+
const tester = rtl.render(
107+
<ProviderMock store={store}>
108+
<Container />
109+
</ProviderMock>
110+
)
111+
112+
store.dispatch({ type: 'NEW_REFERENCE' })
113+
114+
expect(tester.getByTestId('foo')).toHaveTextContent('bar')
115+
116+
// The state should have been mapped twice: on mount and on the dispatch
117+
expect(mapCount).toEqual(2)
118+
119+
// But the render should have been called only on mount since the map state
120+
// did not return a new reference
121+
expect(renderCount).toEqual(1)
122+
})
123+
124+
it('should map state on own props change but not render when the reference does not change', () => {
125+
const staticReference = {
126+
foo: 'bar'
127+
}
128+
129+
let mapCount = 0
130+
let renderCount = 0
131+
132+
const store = createStore(() => staticReference)
133+
134+
function Inner(props) {
135+
renderCount++
136+
return <div data-testid="foo">{JSON.stringify(props)}</div>
137+
}
138+
139+
const Container = connectAdvanced(() => {
140+
return () => {
141+
mapCount++
142+
// return the static reference
143+
return staticReference
144+
}
145+
})(Inner)
146+
147+
class OuterComponent extends Component {
148+
constructor() {
149+
super()
150+
this.state = { foo: 'FOO' }
151+
}
152+
153+
setFoo(foo) {
154+
this.setState({ foo })
155+
}
156+
157+
render() {
158+
return (
159+
<div>
160+
<Container {...this.state} />
161+
</div>
162+
)
163+
}
164+
}
165+
166+
let outerComponent
167+
rtl.render(
168+
<ProviderMock store={store}>
169+
<OuterComponent ref={c => (outerComponent = c)} />
170+
</ProviderMock>
171+
)
172+
173+
outerComponent.setFoo('BAR')
174+
175+
// map on mount and on prop change
176+
expect(mapCount).toEqual(2)
177+
178+
// render only on mount but skip on prop change because no new
179+
// reference was returned
180+
expect(renderCount).toEqual(1)
181+
})
182+
})
183+
})

0 commit comments

Comments
 (0)