Skip to content

Commit 963677f

Browse files
theKasheygregberge
authored andcommitted
fix: fix reconciler warnings (#852)
* Incriment generation only on replacement event(related to #843) * hotRender: drill into the new children branch(fix #843) * tests for the props change case + ci * react-hot-renderer - optional child case
1 parent 7580552 commit 963677f

File tree

8 files changed

+112
-12
lines changed

8 files changed

+112
-12
lines changed

Diff for: examples/styled-components/src/App.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const SmallText = emoStyled('div')`
1515
const indirect = {
1616
element: () => (
1717
<SmallText>
18-
hidden2 <Counter />
18+
hidden <Counter />
1919
</SmallText>
2020
),
2121
}
@@ -24,12 +24,13 @@ const aNumber = 100500
2424

2525
const App = () => (
2626
<h1>
27-
<BigText>1.Hello, world!! {aNumber} </BigText>
27+
<BigText>1.Hello, world! {aNumber} </BigText>
2828
<br />
2929
<SmallText>2.Hello, world.</SmallText>
3030
<br />
3131
<Counter />
3232
<indirect.element />
33+
{aNumber % 2 && <indirect.element />}
3334
</h1>
3435
)
3536

Diff for: src/proxy/utils.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ export function isNativeFunction(fn) {
4444
}
4545

4646
export const identity = a => a
47+
const indirectEval = str => [window.eval][0](str)
4748

4849
export const doesSupportClasses = (function() {
4950
try {
50-
eval('class Test {}')
51+
indirectEval('class Test {}')
5152
return true
5253
} catch (e) {
5354
return false
@@ -56,7 +57,7 @@ export const doesSupportClasses = (function() {
5657

5758
const ES6ProxyComponentFactory =
5859
doesSupportClasses &&
59-
eval(`
60+
indirectEval(`
6061
(function(InitialParent, postConstructionAction) {
6162
return class ProxyComponent extends InitialParent {
6263
constructor(props, context) {

Diff for: src/reactHotLoader.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
updateProxyById,
66
resetProxies,
77
getProxyByType,
8+
getProxyById,
89
createProxyForType,
910
} from './reconciler/proxies'
1011

@@ -27,8 +28,14 @@ const reactHotLoader = {
2728
typeof fileName === 'string' &&
2829
fileName
2930
) {
30-
incrementGeneration()
31-
updateProxyById(`${fileName}#${uniqueLocalName}`, type)
31+
const id = `${fileName}#${uniqueLocalName}`
32+
33+
if (getProxyById(id)) {
34+
// component got replaced. Need to reconsile
35+
incrementGeneration()
36+
}
37+
38+
updateProxyById(id, type)
3239
}
3340
},
3441

Diff for: src/reconciler/hotReplacementRender.js

+23-5
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,18 @@ const mapChildren = (children, instances) => ({
125125
...mapChildren(child, instances[index].children),
126126
}
127127
}
128+
const instanceLine = instances[index] || {}
129+
const oldChildren = asArray(instanceLine.children || [])
130+
const newChildren = asArray((child.props && child.props.children) || [])
131+
const nextChildren =
132+
oldChildren.length && mapChildren(newChildren, oldChildren)
128133
return {
129-
...instances[index],
134+
...instanceLine,
135+
// actually child merge is needed only for TAGs, and usually don't work for Components.
136+
// the children from an instance or rendered children
137+
// while children from a props is just a props.
138+
// they could not exists. they could differ.
139+
...(nextChildren ? { children: nextChildren } : {}),
130140
type: child.type,
131141
}
132142
}),
@@ -146,7 +156,17 @@ const mergeInject = (a, b, instance) => {
146156
if (a.length === b.length) {
147157
return mapChildren(a, b)
148158
}
149-
const flatA = unflatten(a)
159+
160+
// in some cases (no confidence here) B could contain A except null children
161+
// in some cases - could not.
162+
// this depends on React version and the way you build component.
163+
164+
const nonNullA = filterNullArray(a)
165+
if (nonNullA.length === b.length) {
166+
return mapChildren(nonNullA, b)
167+
}
168+
169+
const flatA = unflatten(nonNullA)
150170
const flatB = unflatten(b)
151171
if (flatA.length === flatB.length) {
152172
return mapChildren(flatA, flatB)
@@ -239,9 +259,7 @@ const hotReplacementRender = (instance, stack) => {
239259
next(
240260
// move types from render to the instances of hydrated tree
241261
mergeInject(
242-
filterNullArray(
243-
asArray(child.props ? child.props.children : child.children),
244-
),
262+
asArray(child.props ? child.props.children : child.children),
245263
stackChild.instance.children,
246264
stackChild.instance,
247265
),

Diff for: src/reconciler/proxies.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const generateTypeId = () => `auto-${elementCount++}`
1010

1111
export const getIdByType = type => idsByType.get(type)
1212

13-
export const getProxyByType = type => proxiesByID[getIdByType(type)]
13+
export const getProxyById = id => proxiesByID[id]
14+
export const getProxyByType = type => getProxyById(getIdByType(type))
1415

1516
export const setStandInOptions = options => {
1617
renderOptions = options

Diff for: test/reactHotLoader.test.js

+5
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ describe('reactHotLoader', () => {
108108
it('should increment update counter', () => {
109109
const oldGeneration = getGeneration()
110110
reactHotLoader.register(Div, 'Div', 'reactHotLoader.test.js')
111+
// new thing, no change
112+
expect(getGeneration()).toBe(oldGeneration + 0)
113+
114+
reactHotLoader.register(Div, 'Div', 'reactHotLoader.test.js')
115+
// replacement!
111116
expect(getGeneration()).toBe(oldGeneration + 1)
112117
})
113118

Diff for: test/reconciler.test.js

+37
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const spyComponent = (render, displayName, key) => {
1212
const mounted = jest.fn()
1313
const unmounted = jest.fn()
1414
const willUpdate = jest.fn()
15+
const rendered = jest.fn()
1516

1617
class TestingComponent extends Component {
1718
componentWillMount() {
@@ -34,6 +35,7 @@ const spyComponent = (render, displayName, key) => {
3435
/* eslint-enable */
3536

3637
render() {
38+
rendered()
3739
return render(this.props)
3840
}
3941
}
@@ -48,6 +50,7 @@ const spyComponent = (render, displayName, key) => {
4850
mounted,
4951
unmounted,
5052
key,
53+
rendered,
5154
}
5255
}
5356

@@ -188,6 +191,40 @@ describe('reconciler', () => {
188191
expect(second.mounted).toHaveBeenCalledTimes(0)
189192
})
190193

194+
it('should use new children branch during reconcile', () => {
195+
const First = spyComponent(() => <u>1</u>, 'test', '1')
196+
const Second = spyComponent(() => <u>2</u>, 'test', '2')
197+
198+
const App = ({ second }) => (
199+
<div>
200+
<div>
201+
<First.Component />
202+
{second && <Second.Component />}
203+
</div>
204+
</div>
205+
)
206+
207+
const Mounter = ({ second }) => <App second={second} />
208+
209+
const wrapper = mount(<Mounter second />)
210+
211+
expect(First.rendered).toHaveBeenCalledTimes(1)
212+
expect(Second.rendered).toHaveBeenCalledTimes(1)
213+
214+
incrementGeneration()
215+
wrapper.setProps({ update: 'now' })
216+
expect(First.rendered).toHaveBeenCalledTimes(3)
217+
expect(Second.rendered).toHaveBeenCalledTimes(3)
218+
219+
incrementGeneration()
220+
wrapper.setProps({ second: false })
221+
expect(First.rendered).toHaveBeenCalledTimes(5)
222+
expect(Second.rendered).toHaveBeenCalledTimes(3)
223+
224+
expect(First.unmounted).toHaveBeenCalledTimes(0)
225+
expect(Second.unmounted).toHaveBeenCalledTimes(1)
226+
})
227+
191228
it('should handle function as a child', () => {
192229
const first = spyComponent(
193230
({ children }) => <b>{children(0)}</b>,

Diff for: test/reconciler/proxyAdapter.test.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* eslint-env browser */
2+
import { proxyWrapper } from '../../src/reconciler/proxyAdapter'
3+
import * as proxies from '../../src/reconciler/proxies'
4+
5+
jest.mock('../../src/reconciler/proxies')
6+
7+
proxies.getProxyByType.mockReturnValue({ get: () => 'proxy' })
8+
9+
describe(`proxyAdapter`, () => {
10+
const fn = () => {}
11+
12+
it('should handle empty result', () => {
13+
expect(proxyWrapper()).toBe(undefined)
14+
expect(proxyWrapper(null)).toBe(null)
15+
})
16+
17+
it('should handle arrays', () => {
18+
expect(proxyWrapper([1, 2, 3])).toEqual([1, 2, 3])
19+
expect(proxyWrapper([{ type: fn, prop: 42 }])).toEqual([
20+
{ type: 'proxy', prop: 42 },
21+
])
22+
})
23+
24+
it('should handle elements', () => {
25+
expect(proxyWrapper({ type: fn, prop: 42 })).toEqual({
26+
type: 'proxy',
27+
prop: 42,
28+
})
29+
})
30+
})

0 commit comments

Comments
 (0)