import * as React from "react"; import { useDynamicList, usePositioner } from "./dynamic-hooks"; import { getContainerStyle, defaultGetItemKey } from "./utils"; import type { ListPropsBase, ListItemProps } from "./types"; import type { Positioner } from "./dynamic-hooks"; export function useDynamicListItems({ items, width, height, overscanBy = 2, scrollTop, itemHeightEstimate = 32, positioner, innerRef, as: Container = "div", id, className, style, role = "list", tabIndex, itemAs: WrapperComponent = "div", itemKey = defaultGetItemKey, isScrolling, onRender, render: RenderComponent, }: UseDynamicListItemsOptions) { const children: ( | ListItemProps | React.ReactElement> )[] = useDynamicList({ items, width, height, overscanBy, scrollTop, itemHeightEstimate, positioner, }); const forceUpdate_ = useForceUpdate(); const updating = React.useRef(false); // batches calls to force update updating.current = false; const forceUpdate = () => { if (!updating.current) { updating.current = true; forceUpdate_(); } }; const itemRole = role && role + "item"; let needsFreshBatch = false; let startIndex = 0; let stopIndex: number | undefined; let i = 0; for (; i < children.length; i++) { const child = children[i] as ListItemProps; needsFreshBatch = needsFreshBatch || child.height === -1; if (child.height !== -1) { startIndex = stopIndex === void 0 ? child.index : startIndex; stopIndex = child.index; } children[i] = ( ) as React.ReactElement>; } // If we needed a fresh batch we should reload our components with the measured // sizes React.useEffect(() => { if (needsFreshBatch) forceUpdate(); // eslint-disable-next-line }, [needsFreshBatch]); // Calls the onRender callback if the rendered indices changed React.useEffect(() => { if (typeof onRender === "function" && stopIndex !== void 0) onRender(startIndex, stopIndex, items); // Resets the container key for SSR hydration didEverMount = "1"; // eslint-disable-next-line react-hooks/exhaustive-deps }, [onRender, items, startIndex, stopIndex]); const containerStyle = getContainerStyle( isScrolling, positioner.est(items.length, itemHeightEstimate) ); return ( ); } export interface UseDynamicListItemsOptions extends Omit, "itemGap"> { readonly positioner: Positioner; readonly itemHeightEstimate?: number; readonly render: React.ComponentType>; } export function DynamicList(props: DynamicListProps) { const positioner = usePositioner(props.itemGap); return useDynamicListItems(Object.assign({ positioner }, props)); } let didEverMount = "0"; export interface DynamicListProps extends ListPropsBase { readonly itemHeightEstimate?: number; readonly render: React.ComponentType>; } function DynamicListItem({ role, style, index, data, width, height, as: WrapperComponent, meas, pos, render: RenderComponent, }: DynamicListItemProps & { as: keyof JSX.IntrinsicElements | React.ComponentType; }) { const ref = React.useRef(null); const measure = React.useCallback(() => { const current = ref.current; if (current) { pos.update(index, current.offsetHeight); meas(); } // eslint-disable-next-line }, [pos]); return ( { if (el) { ref.current = el.firstChild as HTMLElement; pos.get(index) === void 0 && pos.set(index, el.offsetHeight); } }} > ); } interface DynamicListItemProps { as: DynamicListProps["itemAs"]; role: string; style: React.CSSProperties; index: number; data: Item; width: number; height: number | undefined; render: React.ComponentType>; pos: Positioner; meas: () => void; } export interface DynamicListRenderProps { index: number; data: Item; width: number; height: number | undefined; measure: () => void; [prop: string]: any; } const useForceUpdate = (): (() => void) => { const setState = React.useState(emptyObj)[1]; return React.useRef(() => setState({})).current; }; const emptyObj = {};