Skip to content

Commit f79a700

Browse files
satya164souhe
authored andcommitted
fix: use React.forwardRef and improve memoization (#41)
1 parent 271a58a commit f79a700

File tree

3 files changed

+28
-202
lines changed

3 files changed

+28
-202
lines changed

src/__tests__/createTheming.test.js

Lines changed: 9 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable react/no-multi-comp */
2-
import React, { Fragment } from 'react';
2+
3+
import * as React from 'react';
34
import ReactDOM from 'react-dom';
45
import createTheming from '../createTheming';
56

@@ -26,8 +27,6 @@ describe('createTheming', () => {
2627
secondaryColor: '#ffffff',
2728
};
2829

29-
const originalTheme = { ...lightTheme };
30-
3130
const { ThemeProvider, withTheme, useTheme } = createTheming(darkTheme);
3231

3332
it('provides theme prop with HOC', () => {
@@ -115,7 +114,7 @@ describe('createTheming', () => {
115114
);
116115
});
117116

118-
it('allows to use ref on wrapped component', () => {
117+
it('sets correct ref on wrapped component', () => {
119118
class Component extends React.Component {
120119
foo() {
121120
return 'bar';
@@ -125,12 +124,11 @@ describe('createTheming', () => {
125124
return null;
126125
}
127126
}
127+
128128
const WithThemeComponent = withTheme(Component);
129129

130130
class Wrapper extends React.Component {
131131
componentDidMount() {
132-
expect(typeof this.comp).toBe('object');
133-
expect(typeof this.comp.foo).toEqual('function');
134132
expect(this.comp.foo()).toEqual('bar');
135133
}
136134

@@ -153,71 +151,6 @@ describe('createTheming', () => {
153151
);
154152
});
155153

156-
it('Should set properly getWrappedInstance method', () => {
157-
class Component extends React.Component {
158-
foo() {
159-
return 'bar';
160-
}
161-
162-
getWrappedInstance() {
163-
return this;
164-
}
165-
166-
render() {
167-
return null;
168-
}
169-
}
170-
class ComponentWithoutGWI extends React.Component {
171-
foo() {
172-
return 'bar';
173-
}
174-
175-
render() {
176-
return null;
177-
}
178-
}
179-
const WithThemeComponent = withTheme(Component);
180-
const WithThemeComponentWithoutGWI = withTheme(ComponentWithoutGWI);
181-
182-
class Wrapper extends React.Component {
183-
componentDidMount() {
184-
expect(typeof this.comp).toBe('object');
185-
expect(typeof this.comp.getWrappedInstance).toEqual('function');
186-
expect(this.comp.getWrappedInstance().foo()).toEqual('bar');
187-
188-
expect(typeof this.compWithoutGWI).toBe('object');
189-
expect(typeof this.compWithoutGWI.getWrappedInstance).toEqual(
190-
'function'
191-
);
192-
expect(this.compWithoutGWI.getWrappedInstance().foo()).toEqual('bar');
193-
}
194-
195-
render() {
196-
return (
197-
<Fragment>
198-
<WithThemeComponent
199-
ref={component => {
200-
this.comp = component;
201-
}}
202-
/>
203-
<WithThemeComponentWithoutGWI
204-
ref={component => {
205-
this.compWithoutGWI = component;
206-
}}
207-
/>
208-
</Fragment>
209-
);
210-
}
211-
}
212-
213-
ReactDOM.render(
214-
<ThemeProvider>
215-
<Wrapper />
216-
</ThemeProvider>,
217-
node
218-
);
219-
});
220-
221154
it('merge theme from provider and prop', () => {
222155
const PropsChecker = withTheme(({ theme }) => {
223156
expect(theme).not.toBe(lightTheme);
@@ -290,26 +223,25 @@ describe('createTheming', () => {
290223
});
291224

292225
it('doesnt mutate existing theme', () => {
226+
const overrides = { primaryColor: 'red' };
293227
const Checker1 = withTheme(({ theme }) => {
294-
expect(theme).toEqual({
295-
...lightTheme,
296-
primaryColor: 'red',
297-
});
228+
expect(theme).not.toBe(lightTheme);
229+
expect(theme).not.toBe(overrides);
298230
return null;
299231
});
300232

301233
const Checker1WithTheme = withTheme(Checker1);
302234

303235
const Checker2 = withTheme(({ theme }) => {
304-
expect(theme).toEqual(originalTheme);
236+
expect(theme).toBe(lightTheme);
305237
return null;
306238
});
307239

308240
const Checker2WithTheme = withTheme(Checker2);
309241

310242
ReactDOM.render(
311243
<ThemeProvider theme={lightTheme}>
312-
<Checker1WithTheme theme={{ primaryColor: 'red' }} />
244+
<Checker1WithTheme theme={overrides} />
313245
<Checker2WithTheme />
314246
</ThemeProvider>,
315247
node

src/createWithTheme.js

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,9 @@ import * as React from 'react';
44
import deepmerge from 'deepmerge';
55
import hoistNonReactStatics from 'hoist-non-react-statics';
66

7-
import { copyRefs } from './utils';
8-
97
import type { ThemeProviderType } from './createThemeProvider';
108
import type { $DeepShape } from './types';
119

12-
const isClassComponent = (Component: any) =>
13-
Boolean(Component.prototype && Component.prototype.isReactComponent);
14-
1510
export type WithThemeType<T> = <P, C: React.ComponentType<P>>(
1611
Comp: C
1712
) => C &
@@ -25,8 +20,6 @@ const createWithTheme = <T: Object, S: $DeepShape<T>>(
2520
) =>
2621
function withTheme(Comp: *) {
2722
class ThemedComponent extends React.Component<*> {
28-
static displayName = `withTheme(${Comp.displayName || Comp.name})`;
29-
3023
_previous: ?{ a: T, b: ?S, result: T };
3124

3225
_merge = (a: T, b: ?S) => {
@@ -36,39 +29,29 @@ const createWithTheme = <T: Object, S: $DeepShape<T>>(
3629
return previous.result;
3730
}
3831

39-
const result = a && b ? deepmerge(a, b) : a || b;
32+
const result = a && b && a !== b ? deepmerge(a, b) : a || b;
4033

4134
this._previous = { a, b, result };
4235

4336
return result;
4437
};
4538

46-
_root: any;
47-
4839
render() {
40+
const { _reactThemeProviderForwardedRef, ...rest } = this.props;
41+
4942
return (
5043
<ThemeContext.Consumer>
5144
{theme => {
52-
const merged = this._merge(theme, this.props.theme);
53-
54-
let element;
55-
if (isClassComponent(Comp)) {
56-
// Only add refs for class components as function components don't support them
57-
// It's needed to support use cases which need access to the underlying node
58-
element = (
59-
<Comp
60-
{...this.props}
61-
ref={c => {
62-
this._root = c;
63-
}}
64-
theme={merged}
65-
/>
66-
);
67-
} else {
68-
element = <Comp {...this.props} theme={merged} />;
69-
}
70-
71-
if (merged !== this.props.theme) {
45+
const merged = this._merge(theme, rest.theme);
46+
const element = (
47+
<Comp
48+
{...rest}
49+
theme={merged}
50+
ref={_reactThemeProviderForwardedRef}
51+
/>
52+
);
53+
54+
if (rest.theme && merged !== rest.theme) {
7255
// If a theme prop was passed, expose it to the children
7356
return <ThemeProvider theme={merged}>{element}</ThemeProvider>;
7457
}
@@ -80,23 +63,15 @@ const createWithTheme = <T: Object, S: $DeepShape<T>>(
8063
}
8164
}
8265

83-
if (isClassComponent(Comp)) {
84-
// getWrappedInstance is exposed by some HOCs like react-redux's connect
85-
// Use it to get the ref to the underlying element
86-
// Also expose it to access the underlying element after wrapping
87-
// $FlowFixMe
88-
ThemedComponent.prototype.getWrappedInstance = function getWrappedInstance() {
89-
return this._root && this._root.getWrappedInstance
90-
? this._root.getWrappedInstance()
91-
: this._root;
92-
};
66+
const ResultComponent = React.forwardRef((props, ref) => (
67+
<ThemedComponent {...props} _reactThemeProviderForwardedRef={ref} />
68+
));
9369

94-
ThemedComponent = copyRefs(ThemedComponent, Comp);
95-
}
70+
ResultComponent.displayName = `withTheme(${Comp.displayName || Comp.name})`;
9671

97-
hoistNonReactStatics(ThemedComponent, Comp);
72+
hoistNonReactStatics(ResultComponent, Comp);
9873

99-
return (ThemedComponent: any);
74+
return (ResultComponent: any);
10075
};
10176

10277
export default createWithTheme;

src/utils.js

Lines changed: 0 additions & 81 deletions
This file was deleted.

0 commit comments

Comments
 (0)