Skip to content

Commit d1144ae

Browse files
authored
Merge pull request #26 from istarkov/master
Themr breaks shouldComponentUpdate shallow equal optimization
2 parents 3d40e06 + 1aaeae3 commit d1144ae

File tree

4 files changed

+134
-20
lines changed

4 files changed

+134
-20
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@
3939
"eslint-plugin-babel": "^3.2.0",
4040
"eslint-plugin-react": "^5.0.1",
4141
"expect": "^1.18.0",
42+
"fbjs": "^0.8.4",
4243
"jsdom": "^8.4.0",
4344
"mocha": "^2.4.5",
4445
"react": "^15.0.1",
4546
"react-addons-test-utils": "^15.0.1",
46-
"rimraf": "^2.5.2"
47+
"react-dom": "^15.3.2",
48+
"rimraf": "^2.5.2",
49+
"sinon": "^1.17.6"
4750
},
4851
"files": [
4952
"lib",

src/components/themr.js

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) =>
4848
composeTheme: optionComposeTheme
4949
}
5050

51+
constructor(...args) {
52+
super(...args)
53+
this.theme_ = this.calcTheme(this.props)
54+
}
55+
5156
getWrappedInstance() {
5257
invariant(optionWithRef,
5358
'To access the wrapped instance, you need to specify ' +
@@ -57,8 +62,8 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) =>
5762
return this.refs.wrappedInstance
5863
}
5964

60-
getNamespacedTheme() {
61-
const { themeNamespace, theme } = this.props
65+
getNamespacedTheme(props) {
66+
const { themeNamespace, theme } = props
6267
if (!themeNamespace) return theme
6368
if (themeNamespace && !theme) throw new Error('Invalid themeNamespace use in react-css-themr. ' +
6469
'themeNamespace prop should be used only with theme prop.')
@@ -68,8 +73,8 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) =>
6873
.reduce((result, key) => ({ ...result, [removeNamespace(key, themeNamespace)]: theme[key] }), {})
6974
}
7075

71-
getThemeNotComposed() {
72-
if (this.props.theme) return this.getNamespacedTheme()
76+
getThemeNotComposed(props) {
77+
if (props.theme) return this.getNamespacedTheme(props)
7378
if (config.localTheme) return config.localTheme
7479
return this.getContextTheme()
7580
}
@@ -80,30 +85,49 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) =>
8085
: {}
8186
}
8287

83-
getTheme() {
84-
return this.props.composeTheme === COMPOSE_SOFTLY
85-
? { ...this.getContextTheme(), ...config.localTheme, ...this.getNamespacedTheme() }
86-
: themeable(themeable(this.getContextTheme(), config.localTheme), this.getNamespacedTheme())
88+
getTheme(props) {
89+
return props.composeTheme === COMPOSE_SOFTLY
90+
? {
91+
...this.getContextTheme(),
92+
...config.localTheme,
93+
...this.getNamespacedTheme(props)
94+
}
95+
: themeable(
96+
themeable(this.getContextTheme(), config.localTheme),
97+
this.getNamespacedTheme(props)
98+
)
99+
}
100+
101+
calcTheme(props) {
102+
const { composeTheme } = props
103+
return composeTheme
104+
? this.getTheme(props)
105+
: this.getThemeNotComposed(props)
106+
}
107+
108+
componentWillReceiveProps(nextProps) {
109+
if (
110+
nextProps.composeTheme !== this.props.composeTheme ||
111+
nextProps.theme !== this.props.theme ||
112+
nextProps.themeNamespace !== this.props.themeNamespace
113+
) {
114+
this.theme_ = this.calcTheme(nextProps)
115+
}
87116
}
88117

89118
render() {
90-
const { composeTheme, ...rest } = this.props
91119
let renderedElement
92120

93121
if (optionWithRef) {
94122
renderedElement = React.createElement(ThemedComponent, {
95-
...rest,
123+
...this.props,
96124
ref: 'wrappedInstance',
97-
theme: composeTheme
98-
? this.getTheme()
99-
: this.getThemeNotComposed()
125+
theme: this.theme_
100126
})
101127
} else {
102128
renderedElement = React.createElement(ThemedComponent, {
103-
...rest,
104-
theme: composeTheme
105-
? this.getTheme()
106-
: this.getThemeNotComposed()
129+
...this.props,
130+
theme: this.theme_
107131
})
108132
}
109133

test/components/ThemeProvider.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ describe('ThemeProvider', () => {
3131
expect(() => TestUtils.renderIntoDocument(
3232
<ThemeProvider theme={theme}>
3333
</ThemeProvider>
34-
)).toThrow(/exactly one child/)
34+
)).toThrow(/expected to receive a single React element child/)
3535

3636
expect(() => TestUtils.renderIntoDocument(
3737
<ThemeProvider theme={theme}>
3838
<div />
3939
<div />
4040
</ThemeProvider>
41-
)).toThrow(/exactly one child/)
41+
)).toThrow(/expected to receive a single React element child/)
4242
} finally {
4343
ThemeProvider.propTypes = propTypes
4444
}

test/components/themr.spec.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import expect from 'expect'
22
import React, { Children, PropTypes, Component } from 'react'
33
import TestUtils from 'react-addons-test-utils'
4+
import sinon from 'sinon'
5+
import { render } from 'react-dom'
6+
import shallowEqual from 'fbjs/lib/shallowEqual'
47
import { themr } from '../../src/index'
58

69
describe('Themr decorator function', () => {
@@ -408,4 +411,88 @@ describe('Themr decorator function', () => {
408411
...bar
409412
})
410413
})
414+
415+
it('should not update theme prop on rerender if nothing changed', () => {
416+
const spy = sinon.stub().returns(<div />)
417+
const div = document.createElement('div')
418+
419+
@themr('Container')
420+
class Container extends Component {
421+
shouldComponentUpdate(nextProps) {
422+
return !shallowEqual(nextProps, this.props)
423+
}
424+
425+
render() {
426+
return spy()
427+
}
428+
}
429+
430+
render(
431+
<Container />,
432+
div
433+
)
434+
435+
render(
436+
<Container />,
437+
div
438+
)
439+
440+
expect(spy.calledOnce).toBe(true)
441+
})
442+
443+
it(
444+
'should update theme prop on rerender if theme or themeNamespace or composeTheme changed',
445+
() => {
446+
const spy = sinon.stub().returns(<div />)
447+
const div = document.createElement('div')
448+
449+
@themr('Container')
450+
class Container extends Component {
451+
shouldComponentUpdate(nextProps) {
452+
return !shallowEqual(nextProps, this.props)
453+
}
454+
455+
render() {
456+
return spy()
457+
}
458+
}
459+
const themeA = {}
460+
const themeB = {}
461+
const themeNamespace = 'nsA'
462+
463+
render(
464+
<Container theme={themeA} />,
465+
div
466+
)
467+
468+
render(
469+
<Container theme={themeB} />,
470+
div
471+
)
472+
473+
expect(spy.calledTwice).toBe(true)
474+
475+
render(
476+
<Container theme={themeB} themeNamespace={themeNamespace} />,
477+
div
478+
)
479+
480+
expect(spy.calledThrice).toBe(true)
481+
482+
483+
render(
484+
<Container theme={themeB} themeNamespace={themeNamespace} composeTheme={'deeply'} />,
485+
div
486+
)
487+
488+
expect(spy.calledThrice).toBe(true)
489+
490+
render(
491+
<Container theme={themeB} themeNamespace={themeNamespace} composeTheme={'softly'} />,
492+
div
493+
)
494+
495+
expect(spy.callCount === 4).toBe(true)
496+
}
497+
)
411498
})

0 commit comments

Comments
 (0)