Skip to content

Native supports a stable style prop reference with React.memo #3150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/styled-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
"babel-plugin-styled-components": ">= 1",
"css-to-react-native": "^3.0.0",
"hoist-non-react-statics": "^3.0.0",
"memoize-one": "^5.1.1",
"react-fast-compare": "^3.1.1",
"shallowequal": "^1.1.0",
"supports-color": "^5.5.0"
},
Expand Down
14 changes: 13 additions & 1 deletion packages/styled-components/src/models/StyledNativeComponent.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @flow
import React, { createElement, Component } from 'react';
import hoist from 'hoist-non-react-statics';
import memoizeOne from 'memoize-one';
import isEqual from 'react-fast-compare';
import merge from '../utils/mixinDeep';
import determineTheme from '../utils/determineTheme';
import { EMPTY_ARRAY, EMPTY_OBJECT } from '../utils/empties';
Expand All @@ -9,6 +11,7 @@ import isFunction from '../utils/isFunction';
import isTag from '../utils/isTag';
import isStyledComponent from '../utils/isStyledComponent';
import { ThemeConsumer } from './ThemeProvider';
import isElementTypeMemo from '../utils/isElementTypeMemo';

import type { Theme } from './ThemeProvider';
import type { Attrs, RuleSet, Target } from '../types';
Expand Down Expand Up @@ -63,7 +66,10 @@ class StyledNativeComponent extends Component<*, *> {
}
}

propsForElement.style = [generatedStyles].concat(style);
const isTargetMemo = !isTargetTag && isElementTypeMemo(elementToBeRendered);
propsForElement.style = isTargetMemo
? this.getMemoStyles(generatedStyles, style)
: this.getStyles(generatedStyles, style);

if (forwardedRef) propsForElement.ref = forwardedRef;
if (forwardedAs) propsForElement.as = forwardedAs;
Expand Down Expand Up @@ -114,6 +120,12 @@ class StyledNativeComponent extends Component<*, *> {
return inlineStyle.generateStyleObject(executionContext);
}

getStyles(generatedStyles, style) {
return [generatedStyles].concat(style);
}

getMemoStyles = memoizeOne((generatedStyles, style) => [generatedStyles].concat(style), isEqual);

setNativeProps(nativeProps: Object) {
if (this.root !== undefined) {
// $FlowFixMe
Expand Down
35 changes: 35 additions & 0 deletions packages/styled-components/src/native/test/native.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,41 @@ Object {
expect(wrapper.root.findByType('Text')).not.toBeUndefined();
});

describe('style prop reference stability on custom components', () => {
it('should not optimize non-React.memo components', () => {
const styleCache = [];
const CustomComponent = ({ style }) => {
styleCache.push(style);
return null;
};
const Comp = styled(CustomComponent)`
margin: 16px;
`;

const wrapper = TestRenderer.create(<Comp />);
wrapper.update(<Comp />);

expect(styleCache.length).toBe(2);
expect(styleCache[0]).not.toBe(styleCache[1]);
});

it('should not cause React.memo(CustomComponent) to re-render', () => {
const styleCache = [];
const CustomComponent = React.memo(({ style }) => {
styleCache.push(style);
return null;
});
const Comp = styled(CustomComponent)`
margin: 16px;
`;

const wrapper = TestRenderer.create(<Comp />);
wrapper.update(<Comp />);

expect(styleCache.length).toBe(1);
});
});

describe('attrs', () => {
beforeEach(() => jest.spyOn(console, 'warn').mockImplementation(() => {}));

Expand Down
18 changes: 18 additions & 0 deletions packages/styled-components/src/utils/isElementTypeMemo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// @flow
import React from 'react';

const hasSymbol = typeof Symbol === 'function' && Symbol.for;

const TypeOfReactMemo = hasSymbol
? Symbol.for('react.memo')
: // $FlowFixMe — accessing impl detail (see https://github.com/facebook/react/issues/12882#issuecomment-440227651)
typeof React.memo === 'function' && React.memo(() => null).$$typeof;

/**
* Determines whether an element type is the result of `React.memo`.
* Note: Use `ReactIs.isMemo()` if you need to test an actual react element (ie. the return of `React.createElement`).
* This fn is in lieu of the currently unmerged https://github.com/facebook/react/pull/15349
*/
export default function isElementTypeMemo(test: any): boolean %checks {
return TypeOfReactMemo && test && test.$$typeof === TypeOfReactMemo;
}
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7081,7 +7081,7 @@ mem@^4.0.0:
mimic-fn "^2.0.0"
p-is-promise "^2.0.0"

memoize-one@^5.0.4:
memoize-one@^5.0.4, memoize-one@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
Expand Down Expand Up @@ -8944,6 +8944,11 @@ react-dom@^16.8.6:
prop-types "^15.6.2"
scheduler "^0.19.1"

react-fast-compare@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.1.1.tgz#0becf31e3812fa70dc231e259f40d892d4767900"
integrity sha512-SCsAORWK59BvauR2L1BTdjQbJcSGJJz03U0awektk2hshLKrITDDFTlgGCqIZpTDlPC/NFlZee6xTMzXPVLiHw==

react-fela@^10.5.0:
version "10.8.2"
resolved "https://registry.yarnpkg.com/react-fela/-/react-fela-10.8.2.tgz#286ee4a273fd53c689a96f4aed38738ab0203cfc"
Expand Down