Skip to content

Commit 708d594

Browse files
authored
Avoid calling component generators on registerComponent (#4286)
* Avoid calling component generators on registerComponent * fix unit tests * remove console.log * fix coverage * fix coverage
1 parent 05b6305 commit 708d594

File tree

13 files changed

+121
-57
lines changed

13 files changed

+121
-57
lines changed

docs/api/Store.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@
1616

1717
---
1818

19-
## setOriginalComponentClassForName
19+
## setComponentClassForName
2020

21-
`setOriginalComponentClassForName(componentName: string, ComponentClass: any): void`
21+
`setComponentClassForName(componentName: string, ComponentClass: any): void`
2222

2323
[source](https://github.com/wix/react-native-navigation/blob/v2/lib/src/components/Store.ts#L15)
2424

2525
---
2626

27-
## getOriginalComponentClassForName
27+
## getComponentClassForName
2828

29-
`getOriginalComponentClassForName(componentName: string): any`
29+
`getComponentClassForName(componentName: string): any`
3030

3131
[source](https://github.com/wix/react-native-navigation/blob/v2/lib/src/components/Store.ts#L19)
3232

integration/redux/Redux.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('redux support', () => {
4141
);
4242
}
4343
};
44-
const CompFromNavigation = Navigation.registerComponent('ComponentName', () => HOC);
44+
const CompFromNavigation = Navigation.registerComponent('ComponentName', () => HOC)();
4545

4646
const tree = renderer.create(<CompFromNavigation componentId='componentId' renderCountIncrement={renderCountIncrement}/>);
4747
expect(tree.toJSON().children).toEqual(['no name']);

integration/remx/remx.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe('remx support', () => {
4848
it('support for static members in connected components', () => {
4949
expect(MyConnectedComponent.options).toEqual({ title: 'MyComponent' });
5050

51-
const registeredComponentClass = Navigation.registerComponent('MyComponentName', () => MyConnectedComponent);
51+
const registeredComponentClass = Navigation.registerComponent('MyComponentName', () => MyConnectedComponent)();
5252
expect(registeredComponentClass.options).toEqual({ title: 'MyComponent' });
5353
});
5454
});

lib/src/Navigation.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import { ComponentProvider } from 'react-native';
1111
import { Element } from './adapters/Element';
1212
import { CommandsObserver } from './events/CommandsObserver';
1313
import { Constants } from './adapters/Constants';
14-
import { ComponentType } from 'react';
1514
import { ComponentEventsObserver } from './events/ComponentEventsObserver';
1615
import { TouchablePreview } from './adapters/TouchablePreview';
1716
import { LayoutRoot, Layout } from './interfaces/Layout';
1817
import { Options } from './interfaces/Options';
18+
import { ComponentWrapper } from './components/ComponentWrapper';
1919

2020
export class Navigation {
2121
public readonly Element: React.ComponentType<{ elementId: any; resizeMode?: any; }>;
@@ -31,15 +31,17 @@ export class Navigation {
3131
private readonly eventsRegistry: EventsRegistry;
3232
private readonly commandsObserver: CommandsObserver;
3333
private readonly componentEventsObserver: ComponentEventsObserver;
34+
private readonly componentWrapper: typeof ComponentWrapper;
3435

3536
constructor() {
3637
this.Element = Element;
3738
this.TouchablePreview = TouchablePreview;
3839
this.store = new Store();
40+
this.componentWrapper = ComponentWrapper;
3941
this.nativeEventsReceiver = new NativeEventsReceiver();
4042
this.uniqueIdProvider = new UniqueIdProvider();
4143
this.componentEventsObserver = new ComponentEventsObserver(this.nativeEventsReceiver);
42-
this.componentRegistry = new ComponentRegistry(this.store, this.componentEventsObserver);
44+
this.componentRegistry = new ComponentRegistry(this.store, this.componentEventsObserver, this.componentWrapper);
4345
this.layoutTreeParser = new LayoutTreeParser();
4446
this.layoutTreeCrawler = new LayoutTreeCrawler(this.uniqueIdProvider, this.store);
4547
this.nativeCommandsSender = new NativeCommandsSender();
@@ -54,7 +56,7 @@ export class Navigation {
5456
* Every navigation component in your app must be registered with a unique name.
5557
* The component itself is a traditional React component extending React.Component.
5658
*/
57-
public registerComponent(componentName: string, getComponentClassFunc: ComponentProvider): ComponentType<any> {
59+
public registerComponent(componentName: string, getComponentClassFunc: ComponentProvider): ComponentProvider {
5860
return this.componentRegistry.registerComponent(componentName, getComponentClassFunc);
5961
}
6062

@@ -67,7 +69,7 @@ export class Navigation {
6769
getComponentClassFunc: ComponentProvider,
6870
ReduxProvider: any,
6971
reduxStore: any
70-
): ComponentType<any> {
72+
): ComponentProvider {
7173
return this.componentRegistry.registerComponent(componentName, getComponentClassFunc, ReduxProvider, reduxStore);
7274
}
7375

lib/src/commands/Commands.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ describe('Commands', () => {
254254
children: []
255255
});
256256
});
257+
258+
it('calls component generator once', async () => {
259+
const generator = jest.fn(() => {
260+
return {};
261+
});
262+
store.setComponentClassForName('theComponentName', generator);
263+
await uut.push('theComponentId', { component: { name: 'theComponentName' } });
264+
expect(generator).toHaveBeenCalledTimes(1);
265+
});
257266
});
258267

259268
describe('pop', () => {

lib/src/commands/LayoutTreeCrawler.test.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,44 @@ describe('LayoutTreeCrawler', () => {
6262
};
6363

6464
const node: any = { type: LayoutType.Component, data: { name: 'theComponentName' } };
65-
store.setOriginalComponentClassForName('theComponentName', MyComponent);
65+
store.setComponentClassForName('theComponentName', () => MyComponent);
6666
uut.crawl(node);
6767
expect(node.data.options).toEqual(theStyle);
6868
});
6969

70+
it('Components: crawl does not cache options', () => {
71+
const optionsWithTitle = (title) => {
72+
return {
73+
topBar: {
74+
title: {
75+
text: title
76+
}
77+
}
78+
}
79+
};
80+
81+
const MyComponent = class {
82+
static options(props) {
83+
return {
84+
topBar: {
85+
title: {
86+
text: props.title
87+
}
88+
}
89+
};
90+
}
91+
};
92+
93+
const node: any = { type: LayoutType.Component, data: { name: 'theComponentName', passProps: { title: 'title' } } };
94+
store.setComponentClassForName('theComponentName', () => MyComponent);
95+
uut.crawl(node);
96+
expect(node.data.options).toEqual(optionsWithTitle('title'));
97+
98+
const node2: any = { type: LayoutType.Component, data: { name: 'theComponentName' } };
99+
uut.crawl(node2);
100+
expect(node2.data.options).toEqual(optionsWithTitle(undefined));
101+
});
102+
70103
it('Components: passes passProps to the static options function to be used by the user', () => {
71104
const MyComponent = class {
72105
static options(passProps) {
@@ -75,7 +108,7 @@ describe('LayoutTreeCrawler', () => {
75108
};
76109

77110
const node: any = { type: LayoutType.Component, data: { name: 'theComponentName', passProps: { bar: { baz: { value: 'hello' } } } } };
78-
store.setOriginalComponentClassForName('theComponentName', MyComponent);
111+
store.setComponentClassForName('theComponentName', () => MyComponent);
79112
uut.crawl(node);
80113
expect(node.data.options).toEqual({ foo: 'hello' });
81114
});
@@ -88,7 +121,7 @@ describe('LayoutTreeCrawler', () => {
88121
};
89122

90123
const node: any = { type: LayoutType.Component, data: { name: 'theComponentName' } };
91-
store.setOriginalComponentClassForName('theComponentName', MyComponent);
124+
store.setComponentClassForName('theComponentName', () => MyComponent);
92125
uut.crawl(node);
93126
expect(node.data.options).toEqual({ foo: {} });
94127
});
@@ -116,7 +149,7 @@ describe('LayoutTreeCrawler', () => {
116149
};
117150

118151
const node = { type: LayoutType.Component, data: { name: 'theComponentName', options: passedOptions } };
119-
store.setOriginalComponentClassForName('theComponentName', MyComponent);
152+
store.setComponentClassForName('theComponentName', () => MyComponent);
120153

121154
uut.crawl(node);
122155

@@ -139,7 +172,7 @@ describe('LayoutTreeCrawler', () => {
139172
};
140173

141174
const node: any = { type: LayoutType.Component, data: { name: 'theComponentName' } };
142-
store.setOriginalComponentClassForName('theComponentName', MyComponent);
175+
store.setComponentClassForName('theComponentName', () => MyComponent);
143176
uut.crawl(node);
144177
expect(node.data.options).not.toBe(theStyle);
145178
});
@@ -153,7 +186,7 @@ describe('LayoutTreeCrawler', () => {
153186
const MyComponent = class { };
154187

155188
const node: any = { type: LayoutType.Component, data: { name: 'theComponentName' } };
156-
store.setOriginalComponentClassForName('theComponentName', MyComponent);
189+
store.setComponentClassForName('theComponentName', () => MyComponent);
157190
uut.crawl(node);
158191
expect(node.data.options).toEqual({});
159192
});

lib/src/commands/LayoutTreeCrawler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class LayoutTreeCrawler {
5252
}
5353

5454
_applyStaticOptions(node) {
55-
const clazz = this.store.getOriginalComponentClassForName(node.data.name) || {};
55+
const clazz = this.store.getComponentClassForName(node.data.name) ? this.store.getComponentClassForName(node.data.name)() : {};
5656
const staticOptions = _.isFunction(clazz.options) ? clazz.options(node.data.passProps || {}) : (_.cloneDeep(clazz.options) || {});
5757
const passedOptions = node.data.options || {};
5858
node.data.options = _.merge({}, staticOptions, passedOptions);

lib/src/components/ComponentRegistry.test.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ describe('ComponentRegistry', () => {
88
let uut;
99
let store;
1010
let mockRegistry: any;
11+
let mockWrapper: any;
1112

12-
class MyComponent extends React.Component {
13+
14+
class WrappedComponent extends React.Component {
1315
render() {
1416
return (
1517
<Text>
@@ -23,30 +25,46 @@ describe('ComponentRegistry', () => {
2325
beforeEach(() => {
2426
store = new Store();
2527
mockRegistry = AppRegistry.registerComponent = jest.fn(AppRegistry.registerComponent);
26-
uut = new ComponentRegistry(store, {} as any);
28+
mockWrapper = jest.mock('./ComponentWrapper');
29+
mockWrapper.wrap = () => WrappedComponent;
30+
uut = new ComponentRegistry(store, {} as any, mockWrapper);
2731
});
2832

29-
it('registers component component by componentName into AppRegistry', () => {
33+
it('registers component by componentName into AppRegistry', () => {
3034
expect(mockRegistry).not.toHaveBeenCalled();
31-
const result = uut.registerComponent('example.MyComponent.name', () => MyComponent);
35+
const result = uut.registerComponent('example.MyComponent.name', () => {});
3236
expect(mockRegistry).toHaveBeenCalledTimes(1);
3337
expect(mockRegistry.mock.calls[0][0]).toEqual('example.MyComponent.name');
34-
expect(mockRegistry.mock.calls[0][1]()).toEqual(result);
38+
expect(mockRegistry.mock.calls[0][1]()).toEqual(result());
3539
});
3640

37-
it('saves the original component into the store', () => {
38-
expect(store.getOriginalComponentClassForName('example.MyComponent.name')).toBeUndefined();
39-
uut.registerComponent('example.MyComponent.name', () => MyComponent);
40-
const Class = store.getOriginalComponentClassForName('example.MyComponent.name');
41+
it('saves the wrapper component generator the store', () => {
42+
expect(store.getComponentClassForName('example.MyComponent.name')).toBeUndefined();
43+
uut.registerComponent('example.MyComponent.name', () => {});
44+
const Class = store.getComponentClassForName('example.MyComponent.name');
4145
expect(Class).not.toBeUndefined();
42-
expect(Class).toEqual(MyComponent);
43-
expect(Object.getPrototypeOf(Class)).toEqual(React.Component);
46+
expect(Class()).toEqual(WrappedComponent);
47+
expect(Object.getPrototypeOf(Class())).toEqual(React.Component);
4448
});
4549

4650
it('resulting in a normal component', () => {
47-
uut.registerComponent('example.MyComponent.name', () => MyComponent);
51+
uut.registerComponent('example.MyComponent.name', () => {});
4852
const Component = mockRegistry.mock.calls[0][1]();
4953
const tree = renderer.create(<Component componentId='123' />);
5054
expect(tree.toJSON()!.children).toEqual(['Hello, World!']);
5155
});
56+
57+
it('should not invoke generator', () => {
58+
const generator = jest.fn(() => {});
59+
uut.registerComponent('example.MyComponent.name', generator);
60+
expect(generator).toHaveBeenCalledTimes(0);
61+
});
62+
63+
it('saves wrapped component to store', () => {
64+
jest.spyOn(store, 'setComponentClassForName');
65+
const generator = jest.fn(() => {});
66+
const componentName = 'example.MyComponent.name';
67+
uut.registerComponent(componentName, generator);
68+
expect(store.getComponentClassForName(componentName)()).toEqual(WrappedComponent);
69+
});
5270
});
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { AppRegistry, ComponentProvider } from 'react-native';
22
import { ComponentWrapper } from './ComponentWrapper';
3-
import { ComponentType } from 'react';
43
import { Store } from './Store';
54
import { ComponentEventsObserver } from '../events/ComponentEventsObserver';
65

76
export class ComponentRegistry {
8-
constructor(private readonly store: Store, private readonly componentEventsObserver: ComponentEventsObserver) { }
7+
constructor(private readonly store: Store, private readonly componentEventsObserver: ComponentEventsObserver, private readonly ComponentWrapperClass: typeof ComponentWrapper) { }
98

10-
registerComponent(componentName: string, getComponentClassFunc: ComponentProvider, ReduxProvider?: any, reduxStore?: any): ComponentType<any> {
11-
const OriginalComponentClass = getComponentClassFunc();
12-
const NavigationComponent = ComponentWrapper.wrap(componentName, OriginalComponentClass, this.store, this.componentEventsObserver, ReduxProvider, reduxStore);
13-
this.store.setOriginalComponentClassForName(componentName, OriginalComponentClass);
14-
AppRegistry.registerComponent(componentName, () => NavigationComponent);
9+
registerComponent(componentName: string, getComponentClassFunc: ComponentProvider, ReduxProvider?: any, reduxStore?: any): ComponentProvider {
10+
const NavigationComponent = () => {
11+
return this.ComponentWrapperClass.wrap(componentName, getComponentClassFunc, this.store, this.componentEventsObserver, ReduxProvider, reduxStore)
12+
};
13+
this.store.setComponentClassForName(componentName, NavigationComponent);
14+
AppRegistry.registerComponent(componentName, NavigationComponent);
1515
return NavigationComponent;
1616
}
1717
}

0 commit comments

Comments
 (0)