diff --git a/__tests__/UrlInput_Performance.test.js b/__tests__/UrlInput_Performance.test.js new file mode 100644 index 0000000..17be2ae --- /dev/null +++ b/__tests__/UrlInput_Performance.test.js @@ -0,0 +1,78 @@ +import React from 'react'; +import {create, act} from 'react-test-renderer'; +import {Text, View} from 'react-native'; +import UrlInput from '../src/components/UrlInput'; + +const mockTextInputRender = jest.fn(); + +// Mock React Native completely to avoid _interopRequireDefault error +jest.mock('react-native', () => { + const RNReact = require('react'); + const TextInput = RNReact.forwardRef((props, ref) => { + mockTextInputRender(props); // Spy on render + return RNReact.createElement('TextInput', {...props, ref}); + }); + + const RNView = props => RNReact.createElement('View', props); + const RNText = props => RNReact.createElement('Text', props); + + return { + TextInput, + View: RNView, + Text: RNText, + }; +}); + +describe('UrlInput Performance', () => { + beforeEach(() => { + mockTextInputRender.mockClear(); + }); + + it('renders unnecessarily when parent updates unrelated state', () => { + let setUnrelated; + const Parent = () => { + const [unrelated, setUnrelatedState] = React.useState(0); + setUnrelated = setUnrelatedState; + + // Stable props + const noop = React.useCallback(() => {}, []); + + return ( + + + {unrelated} + + ); + }; + + act(() => { + create(); + }); + + const initialRenderCount = mockTextInputRender.mock.calls.length; + console.log(`Initial Renders: ${initialRenderCount}`); + + // Expect 1 render initially + expect(initialRenderCount).toBe(1); + + act(() => { + setUnrelated(1); + }); + + const finalRenderCount = mockTextInputRender.mock.calls.length; + console.log(`Final Renders: ${finalRenderCount}`); + + // With optimization, finalRenderCount should be 1. + // Without optimization, it should be 2. + // We want this test to FAIL if optimization is missing (to verify baseline). + // So we assert the OPTIMIZED behavior. + + // NOTE: This assertion will fail now (Expected 1, Received 2). + expect(finalRenderCount).toBe(initialRenderCount); + }); +}); diff --git a/src/App.js b/src/App.js index a5641d9..965ee51 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,17 @@ 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 handleUrlSubmit = useCallback(() => { + searchTextInputRef.current && searchTextInputRef.current.focus(); + }, []); const createPrefetchJobs = async () => { try { @@ -202,9 +206,7 @@ const App = () => { url={url} setUrl={setUrl} persist={persist} - onSubmitEditing={() => - searchTextInputRef.current && searchTextInputRef.current.focus() - } + onSubmitEditing={handleUrlSubmit} /> { ); }; -export default UrlInput; +export default React.memo(UrlInput);