styled-components 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/CODE_OF_CONDUCT.md +1 -1
  3. package/README.md +12 -67
  4. package/dist/styled-components.es.js +273 -110
  5. package/dist/styled-components.js +325 -119
  6. package/dist/styled-components.min.js +2 -2
  7. package/lib/hoc/withTheme.js +13 -7
  8. package/lib/models/BrowserStyleSheet.js +11 -0
  9. package/lib/models/ComponentStyle.js +45 -2
  10. package/lib/models/InlineStyle.js +1 -1
  11. package/lib/models/ServerStyleSheet.js +33 -17
  12. package/lib/models/StyleSheet.js +9 -0
  13. package/lib/models/StyledComponent.js +82 -38
  14. package/lib/models/StyledNativeComponent.js +31 -15
  15. package/lib/models/ThemeProvider.js +44 -12
  16. package/lib/native/index.js +1 -1
  17. package/lib/test/utils.js +5 -2
  18. package/lib/utils/create-broadcast.js +34 -24
  19. package/lib/utils/domElements.js +1 -1
  20. package/lib/utils/flatten.js +4 -1
  21. package/lib/utils/generateAlphabeticName.js +1 -1
  22. package/lib/utils/nonce.js +10 -0
  23. package/lib/utils/once.js +17 -0
  24. package/package.json +10 -10
  25. package/src/hoc/withTheme.js +14 -7
  26. package/src/models/BrowserStyleSheet.js +8 -0
  27. package/src/models/ComponentStyle.js +42 -2
  28. package/src/models/InlineStyle.js +1 -1
  29. package/src/models/ServerStyleSheet.js +27 -12
  30. package/src/models/StyleSheet.js +9 -0
  31. package/src/models/StyledComponent.js +81 -26
  32. package/src/models/StyledNativeComponent.js +30 -10
  33. package/src/models/ThemeProvider.js +38 -9
  34. package/src/models/test/ThemeProvider.test.js +7 -8
  35. package/src/native/index.js +1 -1
  36. package/src/native/test/native.test.js +14 -0
  37. package/src/test/__snapshots__/ssr.test.js.snap +147 -0
  38. package/src/test/expanded-api.test.js +24 -0
  39. package/src/test/props.test.js +14 -3
  40. package/src/test/ssr.test.js +90 -123
  41. package/src/test/styles.test.js +52 -0
  42. package/src/test/utils.js +5 -2
  43. package/src/utils/create-broadcast.js +31 -17
  44. package/src/utils/domElements.js +1 -0
  45. package/src/utils/flatten.js +16 -6
  46. package/src/utils/generateAlphabeticName.js +1 -1
  47. package/src/utils/nonce.js +6 -0
  48. package/src/utils/once.js +12 -0
  49. package/typings/styled-components.d.ts +15 -21
  50. package/typings/tests/issue1068.tsx +226 -0
  51. package/typings/tests/main-test.tsx +1 -1
  52. package/typings/tests/string-tags-test.tsx +62 -0
  53. package/typings/tests/themed-tests/issue1068.tsx +226 -0
  54. package/typings/tests/themed-tests/mytheme-styled-components.tsx +1 -1
  55. package/typings/tests/themed-tests/with-theme-test.tsx +2 -1
  56. package/typings/tests/with-theme-test.tsx +17 -0
  57. package/lib/constructors/test/injectGlobal.test.js +0 -63
  58. package/lib/constructors/test/keyframes.test.js +0 -48
  59. package/lib/constructors/test/styled.test.js +0 -19
  60. package/lib/models/AbstractStyledComponent.js +0 -43
  61. package/lib/models/test/ThemeProvider.test.js +0 -200
  62. package/lib/native/test/native.test.js +0 -290
  63. package/lib/no-parser/test/basic.test.js +0 -46
  64. package/lib/no-parser/test/flatten.test.js +0 -125
  65. package/lib/no-parser/test/keyframes.test.js +0 -45
  66. package/lib/primitives/test/primitives.test.js +0 -289
  67. package/lib/test/attrs.test.js +0 -158
  68. package/lib/test/basic.test.js +0 -267
  69. package/lib/test/css.test.js +0 -43
  70. package/lib/test/expanded-api.test.js +0 -90
  71. package/lib/test/extending.test.js +0 -198
  72. package/lib/test/overriding.test.js +0 -35
  73. package/lib/test/props.test.js +0 -38
  74. package/lib/test/rehydration.test.js +0 -306
  75. package/lib/test/ssr.test.js +0 -187
  76. package/lib/test/styles.test.js +0 -146
  77. package/lib/test/theme.test.js +0 -497
  78. package/lib/test/warnTooManyClasses.test.js +0 -71
  79. package/lib/utils/test/extractCompsFromCSS.test.js +0 -46
  80. package/lib/utils/test/flatten.test.js +0 -109
  81. package/lib/utils/test/generateAlphabeticName.test.js +0 -14
  82. package/lib/utils/test/interleave.test.js +0 -22
  83. package/lib/utils/test/validAttr.test.js +0 -560
  84. package/src/models/AbstractStyledComponent.js +0 -21
  85. package/typings/tags.d.ts +0 -137
@@ -9,7 +9,7 @@ var generateAlphabeticName = function generateAlphabeticName(code) {
9
9
  var name = '';
10
10
  var x = void 0;
11
11
 
12
- for (x = code; x > charsLength; x = Math.floor(x / chars.length)) {
12
+ for (x = code; x > charsLength; x = Math.floor(x / charsLength)) {
13
13
  name = chars[x % charsLength] + name;
14
14
  }
15
15
 
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ exports.__esModule = true;
4
+
5
+ exports.default = function () {
6
+ return typeof __webpack_nonce__ !== 'undefined' ? __webpack_nonce__ : null;
7
+ };
8
+ /* eslint-disable camelcase, no-undef */
9
+
10
+ module.exports = exports['default'];
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+
5
+ // Helper to call a given function, only once
6
+ exports.default = function (cb) {
7
+ var called = false;
8
+
9
+ return function () {
10
+ if (!called) {
11
+ called = true;
12
+ cb();
13
+ }
14
+ };
15
+ };
16
+
17
+ module.exports = exports["default"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "styled-components",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "description": "Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅",
5
5
  "main": "lib/index.js",
6
6
  "typings": "typings/styled-components.d.ts",
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "build": "npm run build:lib && npm run build:dist",
11
11
  "prebuild:lib": "rimraf lib/*",
12
- "build:lib": "babel --out-dir lib src",
12
+ "build:lib": "babel --out-dir lib --ignore \"*.test.js\" src",
13
13
  "prebuild:dist": "rimraf dist/*",
14
14
  "build:dist": "rollup -c && rollup -c --environment ESBUNDLE && rollup -c --environment PRODUCTION",
15
15
  "build:watch": "npm run build:lib -- --watch",
@@ -71,9 +71,9 @@
71
71
  "supports-color": "^3.2.3"
72
72
  },
73
73
  "devDependencies": {
74
- "@types/react": "^15.0.25",
75
- "@types/react-dom": "^15.5.0",
76
- "@types/react-native": "^0.44.4",
74
+ "@types/react": "^15.0.37",
75
+ "@types/react-dom": "^15.5.1",
76
+ "@types/react-native": "^0.46.0",
77
77
  "babel-cli": "^6.22.2",
78
78
  "babel-core": "^6.17.0",
79
79
  "babel-eslint": "^7.1.1",
@@ -87,7 +87,7 @@
87
87
  "babel-plugin-transform-react-remove-prop-types": "^0.4.1",
88
88
  "babel-preset-env": "^1.4.0",
89
89
  "babel-preset-react": "^6.22.0",
90
- "bundlesize": "^0.5.5",
90
+ "bundlesize": "^0.13.2",
91
91
  "chokidar": "^1.6.0",
92
92
  "danger": "^0.16.0",
93
93
  "enzyme": "^2.8.2",
@@ -100,18 +100,17 @@
100
100
  "eslint-plugin-react": "^6.8.0",
101
101
  "express": "^4.14.1",
102
102
  "flow-bin": "^0.47.0",
103
- "flow-copy-source": "^1.1.0",
104
103
  "flow-watch": "^1.1.1",
105
- "jest": "^19.0.2",
104
+ "jest": "^20.0.4",
106
105
  "jsdom": "^9.10.0",
107
106
  "lint-staged": "^3.3.0",
108
107
  "node-watch": "^0.4.1",
109
108
  "pre-commit": "^1.2.2",
110
109
  "react": "^15.5.4",
111
- "react-addons-test-utils": "^15.4.1",
112
110
  "react-dom": "^15.5.4",
113
111
  "react-native": "^0.39.2",
114
112
  "react-primitives": "^0.4.2",
113
+ "react-test-renderer": "^15.6.1",
115
114
  "rimraf": "^2.6.1",
116
115
  "rollup": "0.43.0",
117
116
  "rollup-plugin-babel": "^2.7.1",
@@ -124,12 +123,13 @@
124
123
  "rollup-plugin-uglify": "^1.0.1",
125
124
  "rollup-plugin-visualizer": "^0.1.5",
126
125
  "tslint": "^4.3.1",
127
- "typescript": "^2.3.3"
126
+ "typescript": "^2.4.1"
128
127
  },
129
128
  "peerDependencies": {
130
129
  "react": ">= 0.14.0 < 17.0.0-0"
131
130
  },
132
131
  "jest": {
132
+ "clearMocks": true,
133
133
  "roots": [
134
134
  "<rootDir>/src/"
135
135
  ],
@@ -4,7 +4,7 @@
4
4
  import React from 'react'
5
5
  import PropTypes from 'prop-types'
6
6
  import hoistStatics from 'hoist-non-react-statics'
7
- import { CHANNEL } from '../models/ThemeProvider'
7
+ import { CHANNEL, CHANNEL_NEXT, CONTEXT_CHANNEL_SHAPE } from '../models/ThemeProvider'
8
8
  import _isStyledComponent from '../utils/isStyledComponent'
9
9
 
10
10
  const wrapWithTheme = (Component: ReactClass<any>) => {
@@ -24,24 +24,31 @@ const wrapWithTheme = (Component: ReactClass<any>) => {
24
24
 
25
25
  static contextTypes = {
26
26
  [CHANNEL]: PropTypes.func,
27
+ [CHANNEL_NEXT]: CONTEXT_CHANNEL_SHAPE,
27
28
  };
28
29
 
29
30
  state: { theme?: ?Object } = {};
30
- unsubscribe: () => void;
31
+ unsubscribeId: number = -1
31
32
 
32
33
  componentWillMount() {
33
- if (!this.context[CHANNEL]) {
34
- throw new Error('[withTheme] Please use ThemeProvider to be able to use withTheme')
34
+ const styledContext = this.context[CHANNEL_NEXT]
35
+ if (styledContext === undefined) {
36
+ // eslint-disable-next-line no-console
37
+ console.error('[withTheme] Please use ThemeProvider to be able to use withTheme')
38
+ return
35
39
  }
36
40
 
37
- const subscribe = this.context[CHANNEL]
38
- this.unsubscribe = subscribe(theme => {
41
+
42
+ const { subscribe } = styledContext
43
+ this.unsubscribeId = subscribe(theme => {
39
44
  this.setState({ theme })
40
45
  })
41
46
  }
42
47
 
43
48
  componentWillUnmount() {
44
- if (typeof this.unsubscribe === 'function') this.unsubscribe()
49
+ if (this.unsubscribeId !== -1) {
50
+ this.context[CHANNEL_NEXT].unsubscribe(this.unsubscribeId)
51
+ }
45
52
  }
46
53
 
47
54
  render() {
@@ -1,4 +1,5 @@
1
1
  // @flow
2
+ /* eslint-disable no-underscore-dangle */
2
3
  /*
3
4
  * Browser Style Sheet with Rehydration
4
5
  *
@@ -16,6 +17,7 @@
16
17
  * Note: replace · with * in the above snippet.
17
18
  * */
18
19
  import extractCompsFromCSS from '../utils/extractCompsFromCSS'
20
+ import getNonce from '../utils/nonce'
19
21
  import type { Tag } from './StyleSheet'
20
22
  import StyleSheet, { SC_ATTR, LOCAL_ATTR } from './StyleSheet'
21
23
 
@@ -69,6 +71,12 @@ class BrowserTag implements Tag {
69
71
  const existingNames = this.el.getAttribute(SC_ATTR)
70
72
  this.el.setAttribute(SC_ATTR, existingNames ? `${existingNames} ${name}` : name)
71
73
  }
74
+
75
+ const nonce = getNonce()
76
+
77
+ if (nonce) {
78
+ this.el.setAttribute('nonce', nonce)
79
+ }
72
80
  }
73
81
 
74
82
  toHTML() {
@@ -3,6 +3,24 @@ import hashStr from '../vendor/glamor/hash'
3
3
 
4
4
  import type { RuleSet, NameGenerator, Flattener, Stringifier } from '../types'
5
5
  import StyleSheet from './StyleSheet'
6
+ import isStyledComponent from '../utils/isStyledComponent'
7
+
8
+ const isStaticRules = (rules: RuleSet): boolean => {
9
+ for (let i = 0; i < rules.length; i += 1) {
10
+ const rule = rules[i]
11
+
12
+ // recursive case
13
+ if (Array.isArray(rule) && !isStaticRules(rule)) {
14
+ return false
15
+ } else if (typeof rule === 'function' && !isStyledComponent(rule)) {
16
+ // functions are allowed to be static if they're just being
17
+ // used to get the classname of a nested styled copmonent
18
+ return false
19
+ }
20
+ }
21
+
22
+ return true
23
+ }
6
24
 
7
25
  /*
8
26
  ComponentStyle is all the CSS-specific stuff, not
@@ -12,9 +30,13 @@ export default (nameGenerator: NameGenerator, flatten: Flattener, stringifyRules
12
30
  class ComponentStyle {
13
31
  rules: RuleSet
14
32
  componentId: string
33
+ isStatic: boolean
34
+ lastClassName: ?string
35
+
15
36
 
16
37
  constructor(rules: RuleSet, componentId: string) {
17
38
  this.rules = rules
39
+ this.isStatic = isStaticRules(rules)
18
40
  this.componentId = componentId
19
41
  if (!StyleSheet.instance.hasInjectedComponent(this.componentId)) {
20
42
  const placeholder = process.env.NODE_ENV !== 'production' ? `.${componentId} {}` : ''
@@ -28,16 +50,34 @@ export default (nameGenerator: NameGenerator, flatten: Flattener, stringifyRules
28
50
  * Returns the hash to be injected on render()
29
51
  * */
30
52
  generateAndInjectStyles(executionContext: Object, styleSheet: StyleSheet) {
53
+ const { isStatic, lastClassName } = this
54
+ if (isStatic && lastClassName !== undefined) {
55
+ return lastClassName
56
+ }
57
+
31
58
  const flatCSS = flatten(this.rules, executionContext)
32
59
  const hash = hashStr(this.componentId + flatCSS.join(''))
33
60
 
34
61
  const existingName = styleSheet.getName(hash)
35
- if (existingName) return existingName
62
+ if (existingName !== undefined) {
63
+ if (styleSheet.stylesCacheable) {
64
+ this.lastClassName = existingName
65
+ }
66
+ return existingName
67
+ }
36
68
 
37
69
  const name = nameGenerator(hash)
38
- if (styleSheet.alreadyInjected(hash, name)) return name
70
+ if (styleSheet.stylesCacheable) {
71
+ this.lastClassName = existingName
72
+ }
73
+ if (styleSheet.alreadyInjected(hash, name)) {
74
+ return name
75
+ }
39
76
 
40
77
  const css = `\n${stringifyRules(flatCSS, `.${name}`)}`
78
+ // NOTE: this can only be set when we inject the class-name.
79
+ // For some reason, presumably due to how css is stringifyRules behaves in
80
+ // differently between client and server, styles break.
41
81
  styleSheet.inject(this.componentId, true, css, hash, name)
42
82
  return name
43
83
  }
@@ -33,7 +33,7 @@ export default (styleSheet: StyleSheet) => {
33
33
  root.each(node => {
34
34
  if (node.type === 'decl') {
35
35
  declPairs.push([node.prop, node.value])
36
- } else {
36
+ } else if (node.type !== 'comment') {
37
37
  /* eslint-disable no-console */
38
38
  console.warn(`Node of type ${node.type} not supported as an inline style`)
39
39
  }
@@ -1,8 +1,10 @@
1
1
  // @flow
2
+ /* eslint-disable no-underscore-dangle */
2
3
  import React from 'react'
3
4
  import type { Tag } from './StyleSheet'
4
5
  import StyleSheet, { SC_ATTR, LOCAL_ATTR, clones } from './StyleSheet'
5
6
  import StyleSheetManager from './StyleSheetManager'
7
+ import getNonce from '../utils/nonce'
6
8
 
7
9
  class ServerTag implements Tag {
8
10
  isLocal: boolean
@@ -27,6 +29,10 @@ class ServerTag implements Tag {
27
29
  this.size += 1
28
30
  }
29
31
 
32
+ concatenateCSS() {
33
+ return Object.keys(this.components).reduce((styles, k) => (styles + this.components[k].css), '')
34
+ }
35
+
30
36
  inject(componentId: string, css: string, name: ?string) {
31
37
  const comp = this.components[componentId]
32
38
 
@@ -39,28 +45,37 @@ class ServerTag implements Tag {
39
45
  }
40
46
 
41
47
  toHTML() {
42
- const namesAttr = `${SC_ATTR}="${this.names.join(' ')}"`
43
- const localAttr = `${LOCAL_ATTR}="${this.isLocal ? 'true' : 'false'}"`
44
- const css = Object.keys(this.components)
45
- .map(key => this.components[key].css)
46
- .join('')
48
+ const attrs = [
49
+ 'type="text/css"',
50
+ `${SC_ATTR}="${this.names.join(' ')}"`,
51
+ `${LOCAL_ATTR}="${this.isLocal ? 'true' : 'false'}"`,
52
+ ]
53
+
54
+ const nonce = getNonce()
55
+
56
+ if (nonce) {
57
+ attrs.push(`nonce="${nonce}"`)
58
+ }
47
59
 
48
- return `<style type="text/css" ${namesAttr} ${localAttr}>\n${css}\n</style>`
60
+ return `<style ${attrs.join(' ')}>${this.concatenateCSS()}</style>`
49
61
  }
50
62
 
51
63
  toReactElement(key: string) {
52
- const attributes = {
64
+ const attrs: Object = {
53
65
  [SC_ATTR]: this.names.join(' '),
54
66
  [LOCAL_ATTR]: this.isLocal.toString(),
55
67
  }
56
- const css = Object.keys(this.components)
57
- .map(k => this.components[k].css)
58
- .join('')
68
+
69
+ const nonce = getNonce()
70
+
71
+ if (nonce) {
72
+ attrs.nonce = nonce
73
+ }
59
74
 
60
75
  return (
61
76
  <style
62
- key={key} type="text/css" {...attributes}
63
- dangerouslySetInnerHTML={{ __html: css }}
77
+ key={key} type="text/css" {...attrs}
78
+ dangerouslySetInnerHTML={{ __html: this.concatenateCSS() }}
64
79
  />
65
80
  )
66
81
  }
@@ -30,6 +30,14 @@ export default class StyleSheet {
30
30
  hashes: { [string]: string } = {}
31
31
  deferredInjections: { [string]: string } = {}
32
32
  componentTags: { [string]: Tag }
33
+ // helper for `ComponentStyle` to know when it cache static styles.
34
+ // staticly styled-component can not safely cache styles on the server
35
+ // without all `ComponentStyle` instances saving a reference to the
36
+ // the styleSheet instance they last rendered with,
37
+ // or listening to creation / reset events. otherwise you might create
38
+ // a component with one stylesheet and render it another api response
39
+ // with another, losing styles on from your server-side render.
40
+ stylesCacheable = typeof document !== 'undefined'
33
41
 
34
42
  constructor(tagConstructor: (boolean) => Tag,
35
43
  tags: Array<Tag> = [],
@@ -146,6 +154,7 @@ export default class StyleSheet {
146
154
  return (isServer ? ServerStyleSheet : BrowserStyleSheet).create()
147
155
  }
148
156
 
157
+
149
158
  static clone(oldSheet: StyleSheet) {
150
159
  const newSheet = new StyleSheet(
151
160
  oldSheet.tagConstructor,
@@ -1,6 +1,6 @@
1
1
  // @flow
2
2
 
3
- import { createElement } from 'react'
3
+ import { Component, createElement } from 'react'
4
4
  import PropTypes from 'prop-types'
5
5
 
6
6
  import type { Theme } from './ThemeProvider'
@@ -12,17 +12,20 @@ import isStyledComponent from '../utils/isStyledComponent'
12
12
  import getComponentName from '../utils/getComponentName'
13
13
  import type { RuleSet, Target } from '../types'
14
14
 
15
- import AbstractStyledComponent from './AbstractStyledComponent'
16
- import { CHANNEL } from './ThemeProvider'
15
+ import { CHANNEL, CHANNEL_NEXT, CONTEXT_CHANNEL_SHAPE } from './ThemeProvider'
17
16
  import StyleSheet, { CONTEXT_KEY } from './StyleSheet'
18
17
 
19
18
  const escapeRegex = /[[\].#*$><+~=|^:(),"'`]/g
20
19
  const multiDashRegex = /--+/g
21
20
 
21
+ // HACK for generating all static styles without needing to allocate
22
+ // an empty execution context every single time...
23
+ const STATIC_EXECUTION_CONTEXT = {}
24
+
22
25
  export default (ComponentStyle: Function, constructWithOptions: Function) => {
23
26
  /* We depend on components having unique IDs */
24
27
  const identifiers = {}
25
- const generateId = (_displayName: string) => {
28
+ const generateId = (_displayName: string, parentComponentId: string) => {
26
29
  const displayName = typeof _displayName !== 'string' ?
27
30
  'sc' : _displayName
28
31
  .replace(escapeRegex, '-') // Replace all possible CSS selectors
@@ -32,10 +35,13 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
32
35
  identifiers[displayName] = nr
33
36
 
34
37
  const hash = ComponentStyle.generateName(displayName + nr)
35
- return `${displayName}-${hash}`
38
+ const componentId = `${displayName}-${hash}`
39
+ return parentComponentId !== undefined
40
+ ? `${parentComponentId}-${componentId}`
41
+ : componentId
36
42
  }
37
43
 
38
- class BaseStyledComponent extends AbstractStyledComponent {
44
+ class BaseStyledComponent extends Component {
39
45
  static target: Target
40
46
  static styledComponentId: string
41
47
  static attrs: Object
@@ -47,6 +53,13 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
47
53
  theme: null,
48
54
  generatedClassName: '',
49
55
  }
56
+ unsubscribeId: number = -1
57
+
58
+ unsubscribeFromContext() {
59
+ if (this.unsubscribeId !== -1) {
60
+ this.context[CHANNEL_NEXT].unsubscribe(this.unsubscribeId)
61
+ }
62
+ }
50
63
 
51
64
  buildExecutionContext(theme: any, props: any) {
52
65
  const { attrs } = this.constructor
@@ -66,34 +79,55 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
66
79
  }
67
80
 
68
81
  generateAndInjectStyles(theme: any, props: any) {
69
- const { componentStyle, warnTooManyClasses } = this.constructor
70
- const executionContext = this.buildExecutionContext(theme, props)
82
+ const { attrs, componentStyle, warnTooManyClasses } = this.constructor
71
83
  const styleSheet = this.context[CONTEXT_KEY] || StyleSheet.instance
72
- const className = componentStyle.generateAndInjectStyles(executionContext, styleSheet)
73
84
 
74
- if (warnTooManyClasses !== undefined) warnTooManyClasses(className)
85
+ // staticaly styled-components don't need to build an execution context object,
86
+ // and shouldn't be increasing the number of class names
87
+ if (componentStyle.isStatic && attrs === undefined) {
88
+ return componentStyle.generateAndInjectStyles(STATIC_EXECUTION_CONTEXT, styleSheet)
89
+ } else {
90
+ const executionContext = this.buildExecutionContext(theme, props)
91
+ const className = componentStyle.generateAndInjectStyles(executionContext, styleSheet)
92
+
93
+ if (warnTooManyClasses !== undefined) warnTooManyClasses(className)
75
94
 
76
- return className
95
+ return className
96
+ }
77
97
  }
78
98
 
79
99
  componentWillMount() {
100
+ const { componentStyle } = this.constructor
101
+ const styledContext = this.context[CHANNEL_NEXT]
102
+
103
+ // If this is a staticaly-styled component, we don't need to the theme
104
+ // to generate or build styles.
105
+ if (componentStyle.isStatic) {
106
+ const generatedClassName = this.generateAndInjectStyles(
107
+ STATIC_EXECUTION_CONTEXT,
108
+ this.props,
109
+ )
110
+ this.setState({ generatedClassName })
80
111
  // If there is a theme in the context, subscribe to the event emitter. This
81
112
  // is necessary due to pure components blocking context updates, this circumvents
82
113
  // that by updating when an event is emitted
83
- if (this.context[CHANNEL]) {
84
- const subscribe = this.context[CHANNEL]
85
- this.unsubscribe = subscribe(nextTheme => {
114
+ } else if (styledContext !== undefined) {
115
+ const { subscribe } = styledContext
116
+ this.unsubscribeId = subscribe(nextTheme => {
86
117
  // This will be called once immediately
87
118
 
88
119
  // Props should take precedence over ThemeProvider, which should take precedence over
89
120
  // defaultProps, but React automatically puts defaultProps on props.
90
121
  const { defaultProps } = this.constructor
122
+ /* eslint-disable react/prop-types */
91
123
  const isDefaultTheme = defaultProps && this.props.theme === defaultProps.theme
92
124
  const theme = this.props.theme && !isDefaultTheme ? this.props.theme : nextTheme
125
+ /* eslint-enable */
93
126
  const generatedClassName = this.generateAndInjectStyles(theme, this.props)
94
127
  this.setState({ theme, generatedClassName })
95
128
  })
96
129
  } else {
130
+ // eslint-disable-next-line react/prop-types
97
131
  const theme = this.props.theme || {}
98
132
  const generatedClassName = this.generateAndInjectStyles(
99
133
  theme,
@@ -104,12 +138,21 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
104
138
  }
105
139
 
106
140
  componentWillReceiveProps(nextProps: { theme?: Theme, [key: string]: any }) {
141
+ // If this is a staticaly-styled component, we don't need to listen to
142
+ // props changes to update styles
143
+ const { componentStyle } = this.constructor
144
+ if (componentStyle.isStatic) {
145
+ return
146
+ }
147
+
107
148
  this.setState((oldState) => {
108
149
  // Props should take precedence over ThemeProvider, which should take precedence over
109
150
  // defaultProps, but React automatically puts defaultProps on props.
110
151
  const { defaultProps } = this.constructor
152
+ /* eslint-disable react/prop-types */
111
153
  const isDefaultTheme = defaultProps && nextProps.theme === defaultProps.theme
112
154
  const theme = nextProps.theme && !isDefaultTheme ? nextProps.theme : oldState.theme
155
+ /* eslint-enable */
113
156
  const generatedClassName = this.generateAndInjectStyles(theme, nextProps)
114
157
 
115
158
  return { theme, generatedClassName }
@@ -117,12 +160,11 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
117
160
  }
118
161
 
119
162
  componentWillUnmount() {
120
- if (this.unsubscribe) {
121
- this.unsubscribe()
122
- }
163
+ this.unsubscribeFromContext()
123
164
  }
124
165
 
125
166
  render() {
167
+ // eslint-disable-next-line react/prop-types
126
168
  const { innerRef } = this.props
127
169
  const { generatedClassName } = this.state
128
170
  const { styledComponentId, target } = this.constructor
@@ -130,6 +172,7 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
130
172
  const isTargetTag = isTag(target)
131
173
 
132
174
  const className = [
175
+ // eslint-disable-next-line react/prop-types
133
176
  this.props.className,
134
177
  styledComponentId,
135
178
  this.attrs.className,
@@ -175,7 +218,7 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
175
218
  ) => {
176
219
  const {
177
220
  displayName = isTag(target) ? `styled.${target}` : `Styled(${getComponentName(target)})`,
178
- componentId = generateId(options.displayName),
221
+ componentId = generateId(options.displayName, options.parentComponentId),
179
222
  ParentComponent = BaseStyledComponent,
180
223
  rules: extendingRules,
181
224
  attrs,
@@ -185,7 +228,7 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
185
228
  `${options.displayName}-${options.componentId}` : componentId
186
229
 
187
230
  let warnTooManyClasses
188
- if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {
231
+ if (process.env.NODE_ENV !== 'production') {
189
232
  warnTooManyClasses = createWarnTooManyClasses(displayName)
190
233
  }
191
234
 
@@ -197,6 +240,7 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
197
240
  class StyledComponent extends ParentComponent {
198
241
  static contextTypes = {
199
242
  [CHANNEL]: PropTypes.func,
243
+ [CHANNEL_NEXT]: CONTEXT_CHANNEL_SHAPE,
200
244
  [CONTEXT_KEY]: PropTypes.instanceOf(StyleSheet),
201
245
  }
202
246
 
@@ -208,26 +252,37 @@ export default (ComponentStyle: Function, constructWithOptions: Function) => {
208
252
  static target = target
209
253
 
210
254
  static withComponent(tag) {
211
- const { displayName: _, componentId: __, ...optionsToCopy } = options
212
- const newOptions = { ...optionsToCopy, ParentComponent: StyledComponent }
255
+ const { componentId: previousComponentId, ...optionsToCopy } = options
256
+
257
+ const newComponentId =
258
+ previousComponentId &&
259
+ `${previousComponentId}-${isTag(tag) ? tag : getComponentName(tag)}`
260
+
261
+ const newOptions = {
262
+ ...optionsToCopy,
263
+ componentId: newComponentId,
264
+ ParentComponent: StyledComponent,
265
+ }
266
+
213
267
  return createStyledComponent(tag, newOptions, rules)
214
268
  }
215
269
 
216
270
  static get extend() {
217
271
  const {
218
- displayName: _,
219
- componentId: __,
220
272
  rules: rulesFromOptions,
273
+ componentId: parentComponentId,
221
274
  ...optionsToCopy
222
275
  } = options
223
276
 
224
- const newRules = rulesFromOptions === undefined
225
- ? rules
226
- : rulesFromOptions.concat(rules)
277
+ const newRules =
278
+ rulesFromOptions === undefined
279
+ ? rules
280
+ : rulesFromOptions.concat(rules)
227
281
 
228
282
  const newOptions = {
229
283
  ...optionsToCopy,
230
284
  rules: newRules,
285
+ parentComponentId,
231
286
  ParentComponent: StyledComponent,
232
287
  }
233
288