diff --git a/__tests__/SearchInputBenchmark.test.js b/__tests__/SearchInputBenchmark.test.js new file mode 100644 index 0000000..e0db660 --- /dev/null +++ b/__tests__/SearchInputBenchmark.test.js @@ -0,0 +1,67 @@ +import React, {useState} from 'react'; +import renderer, {act} from 'react-test-renderer'; +import SearchInput from '../src/components/SearchInput'; +import {TextInput} from 'react-native'; + +const mockRenderSpy = jest.fn(); + +// Mock react-native to work with react-test-renderer and spy on TextInput +jest.mock('react-native', () => { + const ReactMock = require('react'); + const TextInputMock = ReactMock.forwardRef((props, ref) => { + mockRenderSpy(); + return ReactMock.createElement('TextInput', {...props, ref}); + }); + return { + TextInput: TextInputMock, + }; +}); + +describe('SearchInput Re-render Benchmark', () => { + it('renders SearchInput only once when parent updates with stable props (optimized)', () => { + const persistMock = jest.fn(); + const setSearchTextMock = jest.fn(); + + const Parent = () => { + const [count, setCount] = useState(0); + + // We simulate a stable persist function here to isolate SearchInput's behavior + // independent of App.js implementation for now. + const stablePersist = persistMock; + + return ( + + + setCount(count + 1)} + /> + + ); + }; + + const component = renderer.create(); + + // Initial render + expect(mockRenderSpy).toHaveBeenCalledTimes(2); // SearchInput's TextInput + Updater TextInput + + // Update state in Parent + const updater = component.root.findAllByType(TextInput)[1]; // The second one is the updater + + mockRenderSpy.mockClear(); + + act(() => { + updater.props.onChangeText('update'); + }); + + // With React.memo, SearchInput should NOT re-render because props are stable. + // SearchInput NO re-render. + // Updater TextInput re-renders. + // So we expect 1 render. + expect(mockRenderSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/App.js b/src/App.js index a5641d9..666c4ff 100644 --- a/src/App.js +++ b/src/App.js @@ -45,6 +45,14 @@ PushNotification.configure({ requestPermissions: true, }); +const persist = async (key, value) => { + try { + await AsyncStorage.setItem(key, value); + } catch (error) { + console.log(error); + } +}; + const App = () => { const [url, setUrl] = useState(''); const [searchText, setSearchText] = useState(''); @@ -114,14 +122,6 @@ const App = () => { loadState(); }, []); - const persist = async (key, value) => { - try { - await AsyncStorage.setItem(key, value); - } catch (error) { - console.log(error); - } - }; - const createPrefetchJobs = async () => { try { setLoading(true); diff --git a/src/components/SearchInput.js b/src/components/SearchInput.js index 491c8d6..9dc67b3 100644 --- a/src/components/SearchInput.js +++ b/src/components/SearchInput.js @@ -1,22 +1,24 @@ -import React, {forwardRef} from 'react'; +import React, {forwardRef, memo} from 'react'; import {TextInput} from 'react-native'; -const SearchInput = forwardRef(({searchText, setSearchText, persist}, ref) => { - return ( - { - setSearchText(text); - }} - onEndEditing={e => { - persist('searchText', e.nativeEvent.text); - }} - value={searchText} - autoCorrect={false} - enablesReturnKeyAutomatically - placeholder="Enter Search String" - ref={ref} - /> - ); -}); +const SearchInput = memo( + forwardRef(({searchText, setSearchText, persist}, ref) => { + return ( + { + setSearchText(text); + }} + onEndEditing={e => { + persist('searchText', e.nativeEvent.text); + }} + value={searchText} + autoCorrect={false} + enablesReturnKeyAutomatically + placeholder="Enter Search String" + ref={ref} + /> + ); + }), +); export default SearchInput;