diff --git a/package.json b/package.json index 5dfbc266..03be9ede 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "rc-animate": "^2.9.1", "react": "^v16.9.0-alpha.0", "react-dom": "^v16.9.0-alpha.0", + "sinon": "^15.0.1", "typescript": "^4.0.0" }, "dependencies": { diff --git a/src/Filler.tsx b/src/Filler.tsx index 4628b22c..14f747fb 100644 --- a/src/Filler.tsx +++ b/src/Filler.tsx @@ -46,14 +46,19 @@ const Filler = React.forwardRef( }; } + const handleResize = React.useCallback( + ({ offsetHeight }) => { + if (offsetHeight && onInnerResize) { + onInnerResize(); + } + }, + [onInnerResize], + ); + return (
{ - if (offsetHeight && onInnerResize) { - onInnerResize(); - } - }} + onResize={handleResize} >
(props: ListProps, ref: React.Ref) { [itemKey], ); - const sharedConfig: SharedConfig = { - getKey, - }; + const sharedConfig: SharedConfig = React.useMemo(() => { + return { + getKey, + }; + }, [getKey]); // ================================ Scroll ================================ function syncScrollTop(newTop: number | ((prev: number) => number)) { diff --git a/src/hooks/useChildren.tsx b/src/hooks/useChildren.tsx index d22ea4c0..47a9e0dd 100644 --- a/src/hooks/useChildren.tsx +++ b/src/hooks/useChildren.tsx @@ -10,17 +10,19 @@ export default function useChildren( renderFunc: RenderFunc, { getKey }: SharedConfig, ) { - return list.slice(startIndex, endIndex + 1).map((item, index) => { - const eleIndex = startIndex + index; - const node = renderFunc(item, eleIndex, { - // style: status === 'MEASURE_START' ? { visibility: 'hidden' } : {}, - }) as React.ReactElement; + return React.useMemo(() => { + return list.slice(startIndex, endIndex + 1).map((item, index) => { + const eleIndex = startIndex + index; + const node = renderFunc(item, eleIndex, { + // style: status === 'MEASURE_START' ? { visibility: 'hidden' } : {}, + }) as React.ReactElement; - const key = getKey(item); - return ( - setNodeRef(item, ele)}> - {node} - - ); - }); + const key = getKey(item); + return ( + setNodeRef(item, ele)}> + {node} + + ); + }); + }, [list, startIndex, endIndex, setNodeRef, renderFunc, getKey]); } diff --git a/src/hooks/useHeights.tsx b/src/hooks/useHeights.tsx index 9a6bb832..5774b8bc 100644 --- a/src/hooks/useHeights.tsx +++ b/src/hooks/useHeights.tsx @@ -15,11 +15,11 @@ export default function useHeights( const heightsRef = useRef(new CacheMap()); const collectRafRef = useRef(); - function cancelRaf() { + const cancelRaf = React.useCallback(function cancelRaf() { raf.cancel(collectRafRef.current); - } + }, []); - function collectHeight() { + const collectHeight = React.useCallback(function () { cancelRaf(); collectRafRef.current = raf(() => { @@ -36,28 +36,31 @@ export default function useHeights( // Always trigger update mark to tell parent that should re-calculate heights when resized setUpdatedMark((c) => c + 1); }); - } - - function setInstanceRef(item: T, instance: HTMLElement) { - const key = getKey(item); - const origin = instanceRef.current.get(key); + }, []); - if (instance) { - instanceRef.current.set(key, instance); - collectHeight(); - } else { - instanceRef.current.delete(key); - } + const setInstanceRef = React.useCallback( + function (item: T, instance: HTMLElement) { + const key = getKey(item); + const origin = instanceRef.current.get(key); - // Instance changed - if (!origin !== !instance) { if (instance) { - onItemAdd?.(item); + instanceRef.current.set(key, instance); + collectHeight(); } else { - onItemRemove?.(item); + instanceRef.current.delete(key); + } + + // Instance changed + if (!origin !== !instance) { + if (instance) { + onItemAdd?.(item); + } else { + onItemRemove?.(item); + } } - } - } + }, + [getKey, onItemAdd, onItemRemove], + ); useEffect(() => { return cancelRaf; diff --git a/tests/props.test.js b/tests/props.test.js index 45edeff2..7b596016 100644 --- a/tests/props.test.js +++ b/tests/props.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { mount } from 'enzyme'; +import sinon from 'sinon'; import List from '../src'; describe('Props', () => { @@ -11,33 +12,47 @@ describe('Props', () => { } const wrapper = mount( - item.id}> + item.id}> {({ id }) => {id}} , ); - expect( - wrapper - .find('Item') - .at(0) - .key(), - ).toBe('903'); - - expect( - wrapper - .find('Item') - .at(1) - .key(), - ).toBe('1128'); + expect(wrapper.find('Item').at(0).key()).toBe('903'); + + expect(wrapper.find('Item').at(1).key()).toBe('1128'); }); it('prefixCls', () => { const wrapper = mount( - id} prefixCls="prefix"> - {id =>
{id}
} + id} prefixCls="prefix"> + {(id) =>
{id}
}
, ); expect(wrapper.find('.prefix-holder-inner').length).toBeTruthy(); }); + + it('no unnecessary re-render', () => { + const renderItem = sinon.fake(({ id, key }) =>
{id}
); + const data = [{ id: 1, key: 1 }]; + function Wrapper() { + const [state, setState] = React.useState(0); + + React.useEffect(() => { + setState(1); + }, []); + + return ( +
+

{state}

+ + {renderItem} + +
+ ); + } + const wrapper = mount(); + expect(wrapper.find('h1').text()).toBe('1'); + expect(renderItem.callCount).toBe(1); + }); }); diff --git a/tests/scroll.test.js b/tests/scroll.test.js index ab5fe46b..eb6276d0 100644 --- a/tests/scroll.test.js +++ b/tests/scroll.test.js @@ -52,7 +52,9 @@ describe('List.Scroll', () => { jest.useFakeTimers(); const listRef = React.createRef(); const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100), ref: listRef }); - jest.runAllTimers(); + act(() => { + jest.runAllTimers(); + }); listRef.current.scrollTo(null); expect(wrapper.find('.rc-virtual-list-scrollbar-thumb').props().style.display).not.toEqual( @@ -65,8 +67,10 @@ describe('List.Scroll', () => { it('value scroll', () => { const listRef = React.createRef(); const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100), ref: listRef }); - listRef.current.scrollTo(903); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo(903); + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop).toEqual(903); wrapper.unmount(); @@ -79,40 +83,56 @@ describe('List.Scroll', () => { describe('index scroll', () => { it('work', () => { - listRef.current.scrollTo({ index: 30, align: 'top' }); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo({ index: 30, align: 'top' }); + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop).toEqual(600); }); it('out of range should not crash', () => { expect(() => { - listRef.current.scrollTo({ index: 99999999999, align: 'top' }); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo({ index: 99999999999, align: 'top' }); + jest.runAllTimers(); + }); }).not.toThrow(); }); }); it('scroll top should not out of range', () => { - listRef.current.scrollTo({ index: 0, align: 'bottom' }); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo({ index: 0, align: 'bottom' }); + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop).toEqual(0); }); it('key scroll', () => { - listRef.current.scrollTo({ key: '30', align: 'bottom' }); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo({ key: '30', align: 'bottom' }); + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop).toEqual(520); }); it('smart', () => { - listRef.current.scrollTo(0); - listRef.current.scrollTo({ index: 30 }); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo(0); + }); + act(() => { + listRef.current.scrollTo({ index: 30 }); + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop).toEqual(520); - listRef.current.scrollTo(800); - listRef.current.scrollTo({ index: 30 }); - jest.runAllTimers(); + act(() => { + listRef.current.scrollTo(800); + }); + act(() => { + listRef.current.scrollTo({ index: 30 }); + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop).toEqual(600); }); }); diff --git a/tests/touch.test.js b/tests/touch.test.js index 12691ab7..49a4477f 100644 --- a/tests/touch.test.js +++ b/tests/touch.test.js @@ -1,4 +1,5 @@ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import { spyElementPrototypes } from './utils/domHook'; import List from '../src'; @@ -55,22 +56,24 @@ describe('List.Touch', () => { return wrapper.find('.rc-virtual-list-holder').instance(); } - // start - const touchEvent = new Event('touchstart'); - touchEvent.touches = [{ pageY: 100 }]; - getElement().dispatchEvent(touchEvent); + act(() => { + // start + const touchEvent = new Event('touchstart'); + touchEvent.touches = [{ pageY: 100 }]; + getElement().dispatchEvent(touchEvent); - // move - const moveEvent = new Event('touchmove'); - moveEvent.touches = [{ pageY: 90 }]; - getElement().dispatchEvent(moveEvent); + // move + const moveEvent = new Event('touchmove'); + moveEvent.touches = [{ pageY: 90 }]; + getElement().dispatchEvent(moveEvent); - // end - const endEvent = new Event('touchend'); - getElement().dispatchEvent(endEvent); + // end + const endEvent = new Event('touchend'); + getElement().dispatchEvent(endEvent); - // smooth - jest.runAllTimers(); + // smooth + jest.runAllTimers(); + }); expect(wrapper.find('ul').instance().scrollTop > 10).toBeTruthy(); wrapper.unmount(); @@ -83,35 +86,40 @@ describe('List.Touch', () => { return wrapper.find('.rc-virtual-list-holder').instance(); } - // start - const touchEvent = new Event('touchstart'); - touchEvent.touches = [{ pageY: 500 }]; - getElement().dispatchEvent(touchEvent); - - // move const preventDefault = jest.fn(); - const moveEvent = new Event('touchmove'); - moveEvent.touches = [{ pageY: 0 }]; - moveEvent.preventDefault = preventDefault; - getElement().dispatchEvent(moveEvent); + + act(() => { + // start + const touchEvent = new Event('touchstart'); + touchEvent.touches = [{ pageY: 500 }]; + getElement().dispatchEvent(touchEvent); + + // move + const moveEvent = new Event('touchmove'); + moveEvent.touches = [{ pageY: 0 }]; + moveEvent.preventDefault = preventDefault; + getElement().dispatchEvent(moveEvent); + }); // Call preventDefault expect(preventDefault).toHaveBeenCalled(); - // ======= Not call since scroll to the bottom ======= - jest.runAllTimers(); - preventDefault.mockReset(); + act(() => { + // ======= Not call since scroll to the bottom ======= + jest.runAllTimers(); + preventDefault.mockReset(); - // start - const touchEvent2 = new Event('touchstart'); - touchEvent2.touches = [{ pageY: 500 }]; - getElement().dispatchEvent(touchEvent2); + // start + const touchEvent2 = new Event('touchstart'); + touchEvent2.touches = [{ pageY: 500 }]; + getElement().dispatchEvent(touchEvent2); - // move - const moveEvent2 = new Event('touchmove'); - moveEvent2.touches = [{ pageY: 0 }]; - moveEvent2.preventDefault = preventDefault; - getElement().dispatchEvent(moveEvent2); + // move + const moveEvent2 = new Event('touchmove'); + moveEvent2.touches = [{ pageY: 0 }]; + moveEvent2.preventDefault = preventDefault; + getElement().dispatchEvent(moveEvent2); + }); expect(preventDefault).not.toHaveBeenCalled(); }); @@ -121,12 +129,11 @@ describe('List.Touch', () => { const preventDefault = jest.fn(); const wrapper = genList({ itemHeight: 20, height: 100, data: genData(100) }); - const touchEvent = new Event('touchstart'); - touchEvent.preventDefault = preventDefault; - wrapper - .find('.rc-virtual-list-scrollbar') - .instance() - .dispatchEvent(touchEvent); + act(() => { + const touchEvent = new Event('touchstart'); + touchEvent.preventDefault = preventDefault; + wrapper.find('.rc-virtual-list-scrollbar').instance().dispatchEvent(touchEvent); + }); expect(preventDefault).toHaveBeenCalled(); });