/* * Copyright 2020 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ import {Collection, Key} from '@react-types/shared'; import {InvalidationContext} from './types'; import {Layout} from './Layout'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Rect} from './Rect'; import {ReusableView} from './ReusableView'; import {Size} from './Size'; import {Virtualizer} from './Virtualizer'; // During SSR, React emits a warning when calling useLayoutEffect. // Since neither useLayoutEffect nor useEffect run on the server, // we can suppress this by replace it with a noop on the server. export const useLayoutEffect: typeof React.useLayoutEffect = typeof document !== 'undefined' ? React.useLayoutEffect : () => {}; interface VirtualizerProps { renderView(type: string, content: T | null): V, layout: Layout, collection: Collection, onVisibleRectChange(rect: Rect): void, persistedKeys?: Set | null, layoutOptions?: O } export interface VirtualizerState { visibleViews: ReusableView[], setVisibleRect: (rect: Rect) => void, contentSize: Size, virtualizer: Virtualizer, isScrolling: boolean, startScrolling: () => void, endScrolling: () => void } export function useVirtualizerState(opts: VirtualizerProps): VirtualizerState { let [visibleRect, setVisibleRect] = useState(new Rect(0, 0, 0, 0)); let [isScrolling, setScrolling] = useState(false); let [invalidationContext, setInvalidationContext] = useState({}); let visibleRectChanged = useRef(false); let [virtualizer] = useState(() => new Virtualizer({ collection: opts.collection, layout: opts.layout, delegate: { setVisibleRect(rect) { setVisibleRect(rect); visibleRectChanged.current = true; }, // TODO: should changing these invalidate the entire cache? renderView: opts.renderView, invalidate: setInvalidationContext } })); // onVisibleRectChange must be called from an effect, not during render. useLayoutEffect(() => { if (visibleRectChanged.current) { visibleRectChanged.current = false; opts.onVisibleRectChange(visibleRect); } }); let mergedInvalidationContext = useMemo(() => { if (opts.layoutOptions != null) { return {...invalidationContext, layoutOptions: opts.layoutOptions}; } return invalidationContext; }, [invalidationContext, opts.layoutOptions]); let visibleViews = virtualizer.render({ layout: opts.layout, collection: opts.collection, persistedKeys: opts.persistedKeys, layoutOptions: opts.layoutOptions, visibleRect, invalidationContext: mergedInvalidationContext, isScrolling }); let contentSize = virtualizer.contentSize; let startScrolling = useCallback(() => { setScrolling(true); }, []); let endScrolling = useCallback(() => { setScrolling(false); }, []); let state = useMemo(() => ({ virtualizer, visibleViews, setVisibleRect, contentSize, isScrolling, startScrolling, endScrolling }), [ virtualizer, visibleViews, setVisibleRect, contentSize, isScrolling, startScrolling, endScrolling ]); return state; }