Skip to content

Commit

Permalink
Merge pull request #2021 from styled-components/inline-hoist
Browse files Browse the repository at this point in the history
inline static hoisting
  • Loading branch information
quantizor committed Sep 21, 2018
2 parents 1baf6b5 + 0f22f46 commit 59c4de2
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 15 deletions.
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -65,7 +65,6 @@
"dependencies": {
"@emotion/is-prop-valid": "^0.6.5",
"css-to-react-native": "^2.0.3",
"hoist-non-react-statics": "^3.0.0",
"memoize-one": "^4.0.0",
"prop-types": "^15.5.4",
"react-is": "^16.3.1",
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Expand Up @@ -42,7 +42,7 @@ const commonPlugins = [
commonjs({
ignoreGlobal: true,
namedExports: {
'react-is': ['isValidElementType'],
'react-is': ['isValidElementType', 'ForwardRef'],
},
}),
replace({
Expand Down
4 changes: 2 additions & 2 deletions src/hoc/withTheme.js
@@ -1,9 +1,9 @@
// @flow
import React, { type ComponentType } from 'react'
import hoistStatics from 'hoist-non-react-statics'
import { ThemeConsumer, type Theme } from '../models/ThemeProvider'
import getComponentName from '../utils/getComponentName'
import determineTheme from '../utils/determineTheme'
import getComponentName from '../utils/getComponentName'
import hoistStatics from '../utils/hoist'

export default (Component: ComponentType<any>) => {
const WithTheme = React.forwardRef((props, ref) => (
Expand Down
2 changes: 1 addition & 1 deletion src/models/StyledComponent.js
@@ -1,14 +1,14 @@
// @flow

import validAttr from '@emotion/is-prop-valid'
import hoist from 'hoist-non-react-statics'
import React, { createElement, PureComponent } from 'react'
import ComponentStyle from './ComponentStyle'
import createWarnTooManyClasses from '../utils/createWarnTooManyClasses'
import determineTheme from '../utils/determineTheme'
import escape from '../utils/escape'
import generateDisplayName from '../utils/generateDisplayName'
import getComponentName from '../utils/getComponentName'
import hoist from '../utils/hoist'
import isFunction from '../utils/isFunction'
import isTag from '../utils/isTag'
import isDerivedReactComponent from '../utils/isDerivedReactComponent'
Expand Down
2 changes: 1 addition & 1 deletion src/models/StyledNativeComponent.js
@@ -1,9 +1,9 @@
// @flow
import hoist from 'hoist-non-react-statics'
import React, { createElement, PureComponent } from 'react'
import determineTheme from '../utils/determineTheme'
import { EMPTY_OBJECT } from '../utils/empties'
import generateDisplayName from '../utils/generateDisplayName'
import hoist from '../utils/hoist'
import isFunction from '../utils/isFunction'
import isTag from '../utils/isTag'
import isDerivedReactComponent from '../utils/isDerivedReactComponent'
Expand Down
105 changes: 105 additions & 0 deletions src/utils/hoist.js
@@ -0,0 +1,105 @@
// @flow
/**
* This is a modified version of hoist-non-react-statics v3.
* BSD License: https://github.com/mridgway/hoist-non-react-statics/blob/master/LICENSE.md
*/
import { ForwardRef } from 'react-is'

const REACT_STATICS = {
childContextTypes: true,
contextTypes: true,
defaultProps: true,
displayName: true,
getDerivedStateFromProps: true,
propTypes: true,
type: true,
}

const KNOWN_STATICS = {
name: true,
length: true,
prototype: true,
caller: true,
callee: true,
arguments: true,
arity: true,
}

const TYPE_STATICS = {
[ForwardRef]: {
$$typeof: true,
render: true,
},
}

const {
defineProperty,
getOwnPropertyNames,
getOwnPropertySymbols = () => [],
getOwnPropertyDescriptor,
getPrototypeOf,
prototype: objectPrototype,
} = Object

const { prototype: arrayPrototype } = Array

export default function hoistNonReactStatics(
targetComponent: any,
sourceComponent: any,
blacklist: ?Object
): any {
if (typeof sourceComponent !== 'string') {
// don't hoist over string (html) components

const inheritedComponent = getPrototypeOf(sourceComponent)

if (inheritedComponent && inheritedComponent !== objectPrototype) {
hoistNonReactStatics(targetComponent, inheritedComponent, blacklist)
}

const keys = arrayPrototype.concat(
getOwnPropertyNames(sourceComponent),
// $FlowFixMe
getOwnPropertySymbols(sourceComponent)
)

const targetStatics =
TYPE_STATICS[targetComponent.$$typeof] || REACT_STATICS

const sourceStatics =
TYPE_STATICS[sourceComponent.$$typeof] || REACT_STATICS

let i = keys.length
let descriptor
let key

// eslint-disable-next-line no-plusplus
while (i--) {
key = keys[i]

if (
// $FlowFixMe
!KNOWN_STATICS[key] &&
!(blacklist && blacklist[key]) &&
!(sourceStatics && sourceStatics[key]) &&
// $FlowFixMe
!(targetStatics && targetStatics[key])
) {
descriptor = getOwnPropertyDescriptor(sourceComponent, key)

if (descriptor) {
try {
// Avoid failures from read-only properties
defineProperty(targetComponent, key, descriptor)
} catch (e) {
/* fail silently */
}
}
}
}

return targetComponent
}

return targetComponent
}
224 changes: 224 additions & 0 deletions src/utils/test/hoist.test.js
@@ -0,0 +1,224 @@
/**
* This is a modified version of the hoist-non-react-statics v3 testing suite.
* BSD License: https://github.com/mridgway/hoist-non-react-statics/blob/master/LICENSE.md
*/
import React from 'react'
import PropTypes from 'prop-types'
import hoistNonReactStatics from '../hoist'

describe('hoist non react statics', () => {
it('should hoist non react statics', () => {
class Component extends React.Component {
static displayName = 'Foo'
static foo = 'bar'
static propTypes = {
on: PropTypes.bool.isRequired,
}

render() {
return null
}
}

class Wrapper extends React.Component {
static displayName = 'Bar'

render() {
return <Component />
}
}

hoistNonReactStatics(Wrapper, Component)

expect(Wrapper.displayName).toEqual('Bar')
expect(Wrapper.foo).toEqual('bar')
})

it('should not hoist custom statics', () => {
class Component extends React.Component {
static displayName = 'Foo'
static foo = 'bar'

render() {
return null
}
}

class Wrapper extends React.Component {
static displayName = 'Bar'

render() {
return <Component />
}
}

hoistNonReactStatics(Wrapper, Component, { foo: true })
expect(Wrapper.foo).toBeUndefined()
})

it('should not hoist statics from strings', () => {
const Component = 'input'

class Wrapper extends React.Component {
render() {
return <Component />
}
}

hoistNonReactStatics(Wrapper, Component)
expect(Wrapper[0]).toBeUndefined() // if hoisting it would equal 'i'
})

it('should hoist symbols', () => {
const foo = Symbol('foo')

class Component extends React.Component {
render() {
return null
}
}

// Manually set static property using Symbol
// since createReactClass doesn't handle symbols passed to static
Component[foo] = 'bar'

class Wrapper extends React.Component {
render() {
return <Component />
}
}

hoistNonReactStatics(Wrapper, Component)

expect(Wrapper[foo]).toEqual('bar')
})

it('should hoist class statics', () => {
class Component extends React.Component {
static foo = 'bar'
static test() {}
}

class Wrapper extends React.Component {
render() {
return <Component />
}
}

hoistNonReactStatics(Wrapper, Component)

expect(Wrapper.foo).toEqual(Component.foo)
expect(Wrapper.test).toEqual(Component.test)
})

it('should hoist properties with accessor methods', () => {
class Component extends React.Component {
render() {
return null
}
}

// Manually set static complex property
// since createReactClass doesn't handle properties passed to static
let counter = 0
Object.defineProperty(Component, 'foo', {
enumerable: true,
configurable: true,
get: () => {
return counter++
},
})

class Wrapper extends React.Component {
render() {
return <Component />
}
}

hoistNonReactStatics(Wrapper, Component)

// Each access of Wrapper.foo should increment counter.
expect(Wrapper.foo).toEqual(0)
expect(Wrapper.foo).toEqual(1)
expect(Wrapper.foo).toEqual(2)
})

it('should inherit static class properties', () => {
class A extends React.Component {
static test3 = 'A'
static test4 = 'D'
test5 = 'foo'
}
class B extends A {
static test2 = 'B'
static test4 = 'DD'
}
class C {
static test1 = 'C'
}
const D = hoistNonReactStatics(C, B)

expect(D.test1).toEqual('C')
expect(D.test2).toEqual('B')
expect(D.test3).toEqual('A')
expect(D.test4).toEqual('DD')
expect(D.test5).toEqual(undefined)
})

it('should inherit static class methods', () => {
class A extends React.Component {
static test3 = 'A'
static test4 = 'D'
static getMeta() {
return {}
}
test5 = 'foo'
}
class B extends A {
static test2 = 'B'
static test4 = 'DD'
static getMeta2() {
return {}
}
}
class C {
static test1 = 'C'
}
const D = hoistNonReactStatics(C, B)

expect(D.test1).toEqual('C')
expect(D.test2).toEqual('B')
expect(D.test3).toEqual('A')
expect(D.test4).toEqual('DD')
expect(D.test5).toEqual(undefined)
expect(D.getMeta).toBeInstanceOf(Function)
expect(D.getMeta2).toBeInstanceOf(Function)
expect(D.getMeta()).toEqual({})
})

it('should not inherit ForwardRef render', () => {
class FancyButton extends React.Component {}
function logProps(Component) {
class LogProps extends React.Component {
static foo = 'foo'
static render = 'bar'
render() {
const { forwardedRef, ...rest } = this.props
return <Component ref={forwardedRef} {...rest} foo="foo" bar="bar" />
}
}
const ForwardedComponent = React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />
})

hoistNonReactStatics(ForwardedComponent, LogProps)

return ForwardedComponent
}

const WrappedFancyButton = logProps(FancyButton)

expect(WrappedFancyButton.foo).toEqual('foo')
expect(WrappedFancyButton.render).not.toEqual('bar')
})
})

0 comments on commit 59c4de2

Please sign in to comment.