Skip to content

Commit d16d417

Browse files
Optimize UrlInput re-renders using useCallback and React.memo (#223)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: xRahul <1639945+xRahul@users.noreply.github.com>
1 parent edc178e commit d16d417

3 files changed

Lines changed: 153 additions & 13 deletions

File tree

__tests__/AppUrlInputPerf.test.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React from 'react';
2+
import renderer, {act} from 'react-test-renderer';
3+
import App from '../src/App';
4+
// We need to import TextInput to access the spy
5+
import {TextInput} from 'react-native';
6+
7+
// Mock dependencies
8+
jest.mock('react-native-background-timer', () => ({
9+
stopBackgroundTimer: jest.fn(),
10+
runBackgroundTimer: jest.fn(),
11+
}));
12+
13+
jest.mock('react-native-push-notification', () => ({
14+
configure: jest.fn(),
15+
localNotification: jest.fn(),
16+
}));
17+
18+
jest.mock('@react-native-community/async-storage', () => ({
19+
setItem: jest.fn(() => Promise.resolve()),
20+
multiSet: jest.fn(() => Promise.resolve()),
21+
getItem: jest.fn(() => Promise.resolve(null)),
22+
getAllKeys: jest.fn(() => Promise.resolve([])),
23+
multiGet: jest.fn(() => Promise.resolve([])),
24+
removeItem: jest.fn(() => Promise.resolve()),
25+
}));
26+
27+
jest.mock('react-native-webview', () => {
28+
return {
29+
WebView: () => null,
30+
};
31+
});
32+
33+
// Fully mock react-native
34+
jest.mock('react-native', () => {
35+
// eslint-disable-next-line no-shadow
36+
const React = require('react');
37+
const View = props => React.createElement('View', props, props.children);
38+
const Text = props => React.createElement('Text', props, props.children);
39+
const ScrollView = props =>
40+
React.createElement('ScrollView', props, props.children);
41+
42+
// Create a spy
43+
const mockSpy = jest.fn();
44+
45+
const TextInputComponent = React.forwardRef((props, ref) => {
46+
mockSpy(props);
47+
return React.createElement('TextInput', {...props, ref});
48+
});
49+
50+
// Attach spy to the component so we can access it
51+
TextInputComponent.mockSpy = mockSpy;
52+
53+
const Switch = props => React.createElement('Switch', props);
54+
const Button = props => React.createElement('Button', props);
55+
const ActivityIndicator = props =>
56+
React.createElement('ActivityIndicator', props);
57+
const Picker = props => React.createElement('Picker', props, props.children);
58+
Picker.Item = props => React.createElement('Picker.Item', props);
59+
const PushNotificationIOS = {
60+
addEventListener: jest.fn(),
61+
removeEventListener: jest.fn(),
62+
requestPermissions: jest.fn(() => Promise.resolve({})),
63+
checkPermissions: jest.fn(),
64+
FetchResult: {NoData: 'NoData'},
65+
};
66+
return {
67+
Platform: {OS: 'ios', select: obj => obj.ios},
68+
View,
69+
Text,
70+
ScrollView,
71+
TextInput: TextInputComponent,
72+
Switch,
73+
Button,
74+
ActivityIndicator,
75+
Picker,
76+
PushNotificationIOS,
77+
StyleSheet: {create: obj => obj, flatten: obj => obj},
78+
};
79+
});
80+
81+
describe('App Performance Benchmark', () => {
82+
beforeEach(() => {
83+
if (TextInput.mockSpy) {
84+
TextInput.mockSpy.mockClear();
85+
}
86+
});
87+
88+
it('does NOT re-render UrlInput when SearchInput changes', async () => {
89+
let component;
90+
91+
// Initial Render
92+
await act(async () => {
93+
component = renderer.create(<App />);
94+
});
95+
96+
// Count UrlInput renders (identified by placeholder)
97+
const initialUrlInputRenders = TextInput.mockSpy.mock.calls.filter(
98+
call => call[0].placeholder === 'Enter URL https://...',
99+
).length;
100+
101+
console.log('Initial UrlInput Renders:', initialUrlInputRenders);
102+
expect(initialUrlInputRenders).toBe(1);
103+
104+
// Reset spy to track subsequent renders ONLY
105+
TextInput.mockSpy.mockClear();
106+
107+
const root = component.root;
108+
const textInputs = root.findAllByType('TextInput'); // This finds the 'TextInput' string element created by mock
109+
110+
// We need to find the element that corresponds to SearchInput's text input.
111+
// The mocked TextInput returns React.createElement('TextInput', ...)
112+
// So finding by type 'TextInput' works.
113+
114+
const searchInput = textInputs.find(
115+
node => node.props.placeholder === 'Enter Search String',
116+
);
117+
118+
expect(searchInput).toBeTruthy();
119+
120+
// Simulate typing in SearchInput
121+
await act(async () => {
122+
searchInput.props.onChangeText('test search');
123+
});
124+
125+
// Count UrlInput renders after update
126+
const finalUrlInputRenders = TextInput.mockSpy.mock.calls.filter(
127+
call => call[0].placeholder === 'Enter URL https://...',
128+
).length;
129+
130+
console.log(
131+
'Final UrlInput Renders (during update):',
132+
finalUrlInputRenders,
133+
);
134+
135+
// If optimized, it should be 0.
136+
expect(finalUrlInputRenders).toBe(0);
137+
});
138+
});

src/App.js

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useState, useEffect, useRef} from 'react';
1+
import React, {useState, useEffect, useRef, useCallback} from 'react';
22
import {
33
Platform,
44
Text,
@@ -45,6 +45,14 @@ PushNotification.configure({
4545
requestPermissions: true,
4646
});
4747

48+
const persist = async (key, value) => {
49+
try {
50+
await AsyncStorage.setItem(key, value);
51+
} catch (error) {
52+
console.log(error);
53+
}
54+
};
55+
4856
const App = () => {
4957
const [url, setUrl] = useState('');
5058
const [searchText, setSearchText] = useState('');
@@ -114,14 +122,6 @@ const App = () => {
114122
loadState();
115123
}, []);
116124

117-
const persist = async (key, value) => {
118-
try {
119-
await AsyncStorage.setItem(key, value);
120-
} catch (error) {
121-
console.log(error);
122-
}
123-
};
124-
125125
const createPrefetchJobs = async () => {
126126
try {
127127
setLoading(true);
@@ -194,6 +194,10 @@ const App = () => {
194194
webViewProps.userAgent = USER_AGENT_DESKTOP;
195195
}
196196

197+
const handleUrlSubmit = useCallback(() => {
198+
searchTextInputRef.current && searchTextInputRef.current.focus();
199+
}, []);
200+
197201
return (
198202
<ScrollView
199203
contentContainerStyle={styles.container}
@@ -202,9 +206,7 @@ const App = () => {
202206
url={url}
203207
setUrl={setUrl}
204208
persist={persist}
205-
onSubmitEditing={() =>
206-
searchTextInputRef.current && searchTextInputRef.current.focus()
207-
}
209+
onSubmitEditing={handleUrlSubmit}
208210
/>
209211

210212
<SearchInput

src/components/UrlInput.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ const UrlInput = ({url, setUrl, persist, onSubmitEditing}) => {
2222
);
2323
};
2424

25-
export default UrlInput;
25+
export default React.memo(UrlInput);

0 commit comments

Comments
 (0)