Skip to content

Commit

Permalink
feat: optional flag to skip animation frame delay in resize observer
Browse files Browse the repository at this point in the history
  • Loading branch information
petyosi committed Aug 1, 2024
1 parent 537e383 commit 14e10f5
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 26 deletions.
28 changes: 23 additions & 5 deletions src/TableVirtuoso.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ const Items = /*#__PURE__*/ React.memo(function VirtuosoItems() {
scrollContainerStateCallback,
log,
undefined,
customScrollParent
customScrollParent,
false,
useEmitterValue('skipAnimationFrameInResizeObserver')
)

const [deviation, setDeviation] = React.useState(0)
Expand Down Expand Up @@ -172,7 +174,11 @@ const Viewport: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
const ctx = React.useContext(VirtuosoMockContext)
const viewportHeight = usePublisher('viewportHeight')
const fixedItemHeight = usePublisher('fixedItemHeight')
const viewportRef = useSize(React.useMemo(() => u.compose(viewportHeight, (el) => correctItemSize(el, 'height')), [viewportHeight]))
const viewportRef = useSize(
React.useMemo(() => u.compose(viewportHeight, (el) => correctItemSize(el, 'height')), [viewportHeight]),
true,
useEmitterValue('skipAnimationFrameInResizeObserver')
)

React.useEffect(() => {
if (ctx) {
Expand All @@ -193,7 +199,11 @@ const WindowViewport: React.FC<React.PropsWithChildren<unknown>> = ({ children }
const windowViewportRect = usePublisher('windowViewportRect')
const fixedItemHeight = usePublisher('fixedItemHeight')
const customScrollParent = useEmitterValue('customScrollParent')
const viewportRef = useWindowViewportRectRef(windowViewportRect, customScrollParent)
const viewportRef = useWindowViewportRectRef(
windowViewportRect,
customScrollParent,
useEmitterValue('skipAnimationFrameInResizeObserver')
)

React.useEffect(() => {
if (ctx) {
Expand All @@ -217,8 +227,16 @@ const TableRoot: React.FC<TableRootProps> = /*#__PURE__*/ React.memo(function Ta
const fixedHeaderContent = useEmitterValue('fixedHeaderContent')
const fixedFooterContent = useEmitterValue('fixedFooterContent')
const context = useEmitterValue('context')
const theadRef = useSize(React.useMemo(() => u.compose(fixedHeaderHeight, (el) => correctItemSize(el, 'height')), [fixedHeaderHeight]))
const tfootRef = useSize(React.useMemo(() => u.compose(fixedFooterHeight, (el) => correctItemSize(el, 'height')), [fixedFooterHeight]))
const theadRef = useSize(
React.useMemo(() => u.compose(fixedHeaderHeight, (el) => correctItemSize(el, 'height')), [fixedHeaderHeight]),
true,
useEmitterValue('skipAnimationFrameInResizeObserver')
)
const tfootRef = useSize(
React.useMemo(() => u.compose(fixedFooterHeight, (el) => correctItemSize(el, 'height')), [fixedFooterHeight]),
true,
useEmitterValue('skipAnimationFrameInResizeObserver')
)
const TheScroller = customScrollParent || useWindowScroll ? WindowScroller : Scroller
const TheViewport = customScrollParent || useWindowScroll ? WindowViewport : Viewport
const TheTable = useEmitterValue('TableComponent')
Expand Down
24 changes: 19 additions & 5 deletions src/Virtuoso.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ const Items = /*#__PURE__*/ React.memo(function VirtuosoItems({ showTopList = fa
log,
listGap,
customScrollParent,
horizontalDirection
horizontalDirection,
useEmitterValue('skipAnimationFrameInResizeObserver')
)

const [deviation, setDeviation] = React.useState(0)
Expand Down Expand Up @@ -244,7 +245,11 @@ const Header: React.FC = /*#__PURE__*/ React.memo(function VirtuosoHeader() {
const Header = useEmitterValue('HeaderComponent')
const headerHeight = usePublisher('headerHeight')
const headerFooterTag = useEmitterValue('headerFooterTag')
const ref = useSize(React.useMemo(() => (el) => headerHeight(correctItemSize(el, 'height')), [headerHeight]))
const ref = useSize(
React.useMemo(() => (el) => headerHeight(correctItemSize(el, 'height')), [headerHeight]),
true,
useEmitterValue('skipAnimationFrameInResizeObserver')
)
const context = useEmitterValue('context')
return Header
? React.createElement(headerFooterTag, { ref }, React.createElement(Header, contextPropIfNotDomElement(Header, context)))
Expand All @@ -255,7 +260,11 @@ const Footer: React.FC = /*#__PURE__*/ React.memo(function VirtuosoFooter() {
const Footer = useEmitterValue('FooterComponent')
const footerHeight = usePublisher('footerHeight')
const headerFooterTag = useEmitterValue('headerFooterTag')
const ref = useSize(React.useMemo(() => (el) => footerHeight(correctItemSize(el, 'height')), [footerHeight]))
const ref = useSize(
React.useMemo(() => (el) => footerHeight(correctItemSize(el, 'height')), [footerHeight]),
true,
useEmitterValue('skipAnimationFrameInResizeObserver')
)
const context = useEmitterValue('context')
return Footer
? React.createElement(headerFooterTag, { ref }, React.createElement(Footer, contextPropIfNotDomElement(Footer, context)))
Expand Down Expand Up @@ -357,7 +366,7 @@ const Viewport: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
() => u.compose(viewportHeight, (el: HTMLElement) => correctItemSize(el, horizontalDirection ? 'width' : 'height')),
[viewportHeight, horizontalDirection]
)
const viewportRef = useSize(viewportSizeCallbackMemo)
const viewportRef = useSize(viewportSizeCallbackMemo, true, useEmitterValue('skipAnimationFrameInResizeObserver'))

React.useEffect(() => {
if (ctx) {
Expand All @@ -378,7 +387,11 @@ const WindowViewport: React.FC<React.PropsWithChildren<unknown>> = ({ children }
const windowViewportRect = usePublisher('windowViewportRect')
const fixedItemHeight = usePublisher('fixedItemHeight')
const customScrollParent = useEmitterValue('customScrollParent')
const viewportRef = useWindowViewportRectRef(windowViewportRect, customScrollParent)
const viewportRef = useWindowViewportRectRef(
windowViewportRect,
customScrollParent,
useEmitterValue('skipAnimationFrameInResizeObserver')
)
const alignToBottom = useEmitterValue('alignToBottom')

React.useEffect(() => {
Expand Down Expand Up @@ -465,6 +478,7 @@ export const {
scrollerRef: 'scrollerRef',
logLevel: 'logLevel',
horizontalDirection: 'horizontalDirection',
skipAnimationFrameInResizeObserver: 'skipAnimationFrameInResizeObserver',
},
methods: {
scrollToIndex: 'scrollToIndex',
Expand Down
22 changes: 17 additions & 5 deletions src/VirtuosoGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ const GridItems: React.FC = /*#__PURE__*/ React.memo(function GridItems() {
})
},
[scrollHeightCallback, itemDimensions, gridGap, log]
)
),
true,
false
)

if (stateRestoreInProgress) {
Expand Down Expand Up @@ -129,7 +131,11 @@ const Header: React.FC = React.memo(function VirtuosoHeader() {
const Header = useEmitterValue('HeaderComponent')
const headerHeight = usePublisher('headerHeight')
const headerFooterTag = useEmitterValue('headerFooterTag')
const ref = useSize(React.useMemo(() => (el) => headerHeight(correctItemSize(el, 'height')), [headerHeight]))
const ref = useSize(
React.useMemo(() => (el) => headerHeight(correctItemSize(el, 'height')), [headerHeight]),
true,
false
)
const context = useEmitterValue('context')
return Header
? React.createElement(headerFooterTag, { ref }, React.createElement(Header, contextPropIfNotDomElement(Header, context)))
Expand All @@ -140,7 +146,11 @@ const Footer: React.FC = React.memo(function VirtuosoGridFooter() {
const Footer = useEmitterValue('FooterComponent')
const footerHeight = usePublisher('footerHeight')
const headerFooterTag = useEmitterValue('headerFooterTag')
const ref = useSize(React.useMemo(() => (el) => footerHeight(correctItemSize(el, 'height')), [footerHeight]))
const ref = useSize(
React.useMemo(() => (el) => footerHeight(correctItemSize(el, 'height')), [footerHeight]),
true,
false
)
const context = useEmitterValue('context')
return Footer
? React.createElement(headerFooterTag, { ref }, React.createElement(Footer, contextPropIfNotDomElement(Footer, context)))
Expand All @@ -158,7 +168,9 @@ const Viewport: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
viewportDimensions(el.getBoundingClientRect())
},
[viewportDimensions]
)
),
true,
false
)

React.useEffect(() => {
Expand All @@ -180,7 +192,7 @@ const WindowViewport: React.FC<React.PropsWithChildren<unknown>> = ({ children }
const windowViewportRect = usePublisher('windowViewportRect')
const itemDimensions = usePublisher('itemDimensions')
const customScrollParent = useEmitterValue('customScrollParent')
const viewportRef = useWindowViewportRectRef(windowViewportRect, customScrollParent)
const viewportRef = useWindowViewportRectRef(windowViewportRect, customScrollParent, false)

React.useEffect(() => {
if (ctx) {
Expand Down
7 changes: 7 additions & 0 deletions src/component-interfaces/Virtuoso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,13 @@ export interface VirtuosoProps<D, C> extends ListRootProps {
* When set, turns the scroller into a horizontal list. The items are positioned with `inline-block`.
*/
horizontalDirection?: boolean

/**
* When set, the resize observer used to measure the items will not use `requestAnimationFrame` to report the size changes.
* Setting this to true will improve performance and reduce flickering, but will cause benign errors to be reported in the console if the size of the items changes while they are being measured.
* See https://github.com/petyosi/react-virtuoso/issues/1049 for more information.
*/
skipAnimationFrameInResizeObserver?: boolean
}

export interface GroupedVirtuosoProps<D, C> extends Omit<VirtuosoProps<D, C>, 'totalCount' | 'itemContent'> {
Expand Down
2 changes: 2 additions & 0 deletions src/domIOSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const domIOSystem = u.system(
const scrollBy = u.stream<ScrollToOptions>()
const scrollingInProgress = u.statefulStream(false)
const horizontalDirection = u.statefulStream(false)
const skipAnimationFrameInResizeObserver = u.statefulStream(false)

u.connect(
u.pipe(
Expand Down Expand Up @@ -49,6 +50,7 @@ export const domIOSystem = u.system(
scrollHeight,
smoothScrollTargetReached,
horizontalDirection,
skipAnimationFrameInResizeObserver,

// signals
scrollTo,
Expand Down
9 changes: 5 additions & 4 deletions src/hooks/useChangedChildSizes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ export default function useChangedListContentsSizes(
enabled: boolean,
scrollContainerStateCallback: (state: ScrollContainerState) => void,
log: Log,
gap?: (gap: number) => void,
customScrollParent?: HTMLElement,
horizontalDirection?: boolean
gap: ((gap: number) => void) | undefined,
customScrollParent: HTMLElement | undefined,
horizontalDirection: boolean,
skipAnimationFrame: boolean
) {
const memoedCallback = React.useCallback(
(el: HTMLElement) => {
Expand Down Expand Up @@ -79,7 +80,7 @@ export default function useChangedListContentsSizes(
[callback, itemSize, log, gap, customScrollParent, scrollContainerStateCallback]
)

return useSizeWithElRef(memoedCallback, enabled)
return useSizeWithElRef(memoedCallback, enabled, skipAnimationFrame)
}

function getChangedChildSizes(children: HTMLCollection, itemSize: SizeFunction, field: 'offsetHeight' | 'offsetWidth', log: Log) {
Expand Down
11 changes: 6 additions & 5 deletions src/hooks/useSize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'

export type CallbackRefParam = HTMLElement | null

export function useSizeWithElRef(callback: (e: HTMLElement) => void, enabled = true) {
export function useSizeWithElRef(callback: (e: HTMLElement) => void, enabled: boolean, skipAnimationFrame: boolean) {
const ref = React.useRef<CallbackRefParam>(null)

let callbackRef = (_el: CallbackRefParam) => {
Expand All @@ -12,12 +12,13 @@ export function useSizeWithElRef(callback: (e: HTMLElement) => void, enabled = t
if (typeof ResizeObserver !== 'undefined') {
const observer = React.useMemo(() => {
return new ResizeObserver((entries: ResizeObserverEntry[]) => {
requestAnimationFrame(() => {
const code = () => {
const element = entries[0].target as HTMLElement
if (element.offsetParent !== null) {
callback(element)
}
})
}
skipAnimationFrame ? code() : requestAnimationFrame(code)
})
}, [callback])

Expand All @@ -37,6 +38,6 @@ export function useSizeWithElRef(callback: (e: HTMLElement) => void, enabled = t
return { ref, callbackRef }
}

export default function useSize(callback: (e: HTMLElement) => void, enabled = true) {
return useSizeWithElRef(callback, enabled).callbackRef
export default function useSize(callback: (e: HTMLElement) => void, enabled: boolean, skipAnimationFrame: boolean) {
return useSizeWithElRef(callback, enabled, skipAnimationFrame).callbackRef
}
8 changes: 6 additions & 2 deletions src/hooks/useWindowViewportRect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import React from 'react'
import { useSizeWithElRef } from './useSize'
import { WindowViewportInfo } from '../interfaces'

export default function useWindowViewportRectRef(callback: (info: WindowViewportInfo) => void, customScrollParent?: HTMLElement) {
export default function useWindowViewportRectRef(
callback: (info: WindowViewportInfo) => void,
customScrollParent: HTMLElement | undefined,
skipAnimationFrame: boolean
) {
const viewportInfo = React.useRef<WindowViewportInfo | null>(null)

const calculateInfo = React.useCallback(
Expand Down Expand Up @@ -36,7 +40,7 @@ export default function useWindowViewportRectRef(callback: (info: WindowViewport
[callback, customScrollParent]
)

const { callbackRef, ref } = useSizeWithElRef(calculateInfo)
const { callbackRef, ref } = useSizeWithElRef(calculateInfo, true, skipAnimationFrame)

const scrollAndResizeEventHandler = React.useCallback(() => {
calculateInfo(ref.current)
Expand Down

0 comments on commit 14e10f5

Please sign in to comment.