diff --git a/__tests__/AppOptimization.test.js b/__tests__/AppOptimization.test.js
new file mode 100644
index 0000000..87e7a35
--- /dev/null
+++ b/__tests__/AppOptimization.test.js
@@ -0,0 +1,139 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+// We don't import TextInput from react-native here because we are mocking it.
+// We will access it via the mock or just by string type if necessary.
+
+// Mock dependencies
+jest.mock('react-native-background-timer', () => ({
+ stopBackgroundTimer: jest.fn(),
+ runBackgroundTimer: jest.fn(),
+}));
+
+jest.mock('react-native-push-notification', () => ({
+ configure: jest.fn(),
+ localNotification: jest.fn(),
+}));
+
+jest.mock('@react-native-community/async-storage', () => ({
+ setItem: jest.fn(() => Promise.resolve()),
+ multiSet: jest.fn(() => Promise.resolve()),
+ getItem: jest.fn(() => Promise.resolve(null)),
+ getAllKeys: jest.fn(() => Promise.resolve([])),
+ multiGet: jest.fn(() => Promise.resolve([])),
+ removeItem: jest.fn(() => Promise.resolve()),
+}));
+
+jest.mock('react-native-webview', () => {
+ return {
+ WebView: () => null,
+ };
+});
+
+jest.mock('../src/services/BackgroundService', () => ({
+ checkUrlForText: jest.fn(),
+ background_task: jest.fn(),
+}));
+
+// Fully mock react-native to avoid renderer issues
+jest.mock('react-native', () => {
+ // eslint-disable-next-line no-shadow
+ const React = require('react');
+ const View = props => React.createElement('View', props, props.children);
+ const Text = props => React.createElement('Text', props, props.children);
+ const ScrollView = props =>
+ React.createElement('ScrollView', props, props.children);
+ const TextInput = React.forwardRef((props, ref) =>
+ React.createElement('TextInput', {...props, ref}),
+ );
+ const Switch = props => React.createElement('Switch', props);
+ const Button = props => React.createElement('Button', props);
+ const ActivityIndicator = props =>
+ React.createElement('ActivityIndicator', props);
+
+ const Picker = props => React.createElement('Picker', props, props.children);
+ Picker.Item = props => React.createElement('Picker.Item', props);
+
+ const PushNotificationIOS = {
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ requestPermissions: jest.fn(() => Promise.resolve({})),
+ checkPermissions: jest.fn(),
+ FetchResult: {
+ NoData: 'NoData',
+ NewData: 'NewData',
+ Failed: 'Failed',
+ },
+ };
+
+ return {
+ Platform: {
+ OS: 'ios',
+ select: obj => obj.ios,
+ },
+ View,
+ Text,
+ ScrollView,
+ TextInput,
+ Switch,
+ Button,
+ ActivityIndicator,
+ Picker,
+ PushNotificationIOS,
+ StyleSheet: {
+ create: obj => obj,
+ flatten: obj => obj,
+ },
+ };
+});
+
+// Import App after mocking react-native
+const App = require('../src/App').default;
+
+it('Case Sensitive Search handler reference check', () => {
+ const component = renderer.create();
+ const root = component.root;
+
+ const findSettingsSwitch = () =>
+ root.findAll(
+ node =>
+ node.type.name === 'SettingsSwitch' &&
+ node.props.label === 'Case Sensitive Search:',
+ )[0];
+
+ let switchComp = findSettingsSwitch();
+ const handler1 = switchComp.props.onValueChange;
+
+ // Find the TextInput. Since we mocked it as 'TextInput', we can find it by type 'TextInput'
+ // But wait, the mock returns a React component.
+ // In the mock: const TextInput = React.forwardRef...
+ // So we need to find that component.
+
+ // We can find by prop if needed, or by type name if the mock sets displayName.
+ // Or we can just inspect the tree.
+
+ // Let's rely on finding by type name "TextInput" if react-test-renderer supports it for functional components.
+ // Or better, we can find by props passed to it. UrlInput passes 'autoCapitalize="none"'.
+
+ // The mock uses createElement('TextInput'...), so the type in the output tree will be 'TextInput'.
+ // However, the component in the tree is the Mocked TextInput.
+
+ // Let's filter by checking if it has onChangeText
+ const textInputs = root.findAll(node => node.props.onChangeText);
+ const urlInput = textInputs[0];
+
+ renderer.act(() => {
+ urlInput.props.onChangeText('new url');
+ });
+
+ // Re-find the switch component
+ switchComp = findSettingsSwitch();
+ const handler2 = switchComp.props.onValueChange;
+
+ if (handler1 === handler2) {
+ console.log('Handler is stable (OPTIMIZED)');
+ } else {
+ console.log('Handler is new instance (UNOPTIMIZED)');
+ }
+
+ expect(handler1).toBe(handler2);
+});
diff --git a/src/App.js b/src/App.js
index a5641d9..23a8ed5 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,4 +1,4 @@
-import React, {useState, useEffect, useRef} from 'react';
+import React, {useState, useEffect, useRef, useCallback} from 'react';
import {
Platform,
Text,
@@ -114,13 +114,13 @@ const App = () => {
loadState();
}, []);
- const persist = async (key, value) => {
+ const persist = useCallback(async (key, value) => {
try {
await AsyncStorage.setItem(key, value);
} catch (error) {
console.log(error);
}
- };
+ }, []);
const createPrefetchJobs = async () => {
try {
@@ -189,6 +189,24 @@ const App = () => {
refreshWebView();
};
+ const handleCaseSensitiveChange = useCallback(
+ value => {
+ const valStr = value ? 'yes' : 'no';
+ setCaseSensitiveSearch(valStr);
+ persist('caseSensitiveSearch', valStr);
+ },
+ [persist],
+ );
+
+ const handleSearchAbsenceChange = useCallback(
+ value => {
+ const valStr = value ? 'yes' : 'no';
+ setSearchAbsence(valStr);
+ persist('searchAbsence', valStr);
+ },
+ [persist],
+ );
+
const webViewProps = {};
if (webPlatformType === WEB_PLATFORM_DESKTOP) {
webViewProps.userAgent = USER_AGENT_DESKTOP;
@@ -217,21 +235,13 @@ const App = () => {
{
- const valStr = value ? 'yes' : 'no';
- setCaseSensitiveSearch(valStr);
- persist('caseSensitiveSearch', valStr);
- }}
+ onValueChange={handleCaseSensitiveChange}
/>
{
- const valStr = value ? 'yes' : 'no';
- setSearchAbsence(valStr);
- persist('searchAbsence', valStr);
- }}
+ onValueChange={handleSearchAbsenceChange}
/>