web: improve list item rendering performance

This commit is contained in:
Abdullah Atta
2023-12-11 11:00:31 +05:00
committed by Abdullah Atta
parent 159c4454a3
commit 05ccfcd9fb
4 changed files with 115 additions and 67 deletions

View File

@@ -38,7 +38,7 @@ import {
isGroupHeader
} from "@notesnook/core";
import { VirtualizedList } from "../virtualized-list";
import { Virtualizer } from "@tanstack/react-virtual";
import { ScrollToOptions, Virtualizer } from "@tanstack/react-virtual";
import { useResolvedItem } from "./resolved-item";
type ListContainerProps = {
@@ -138,64 +138,18 @@ function ListContainer(props: ListContainerProps) {
onFocus: () => onFocus(index),
onMouseDown: (e) => onMouseDown(e.nativeEvent, index)
})}
renderItem={function ItemRenderer({ index }) {
const resolvedItem = useResolvedItem({ index, items });
// console.log("Rendering:", index);
if (!resolvedItem)
return <div style={{ height: 50, width: "100%" }}></div>;
return (
<>
{resolvedItem.group && group ? (
<GroupHeader
groupingKey={group}
refresh={refresh}
title={resolvedItem.group.title}
isFocused={index === focusedGroupIndex}
index={index}
onSelectGroup={() => {
let endIndex;
for (
let i = index + 1;
i < props.items.ids.length;
++i
) {
if (typeof props.items.ids[i] === "object") {
endIndex = i;
break;
}
}
setSelectedItems([
...selectionStore.get().selectedItems,
...props.items.ids.slice(
index,
endIndex || props.items.ids.length
)
]);
}}
groups={async () =>
items.groups ? items.groups() : []
}
onJump={(index) => {
listRef.current?.scrollToIndex(index, {
align: "center",
behavior: "auto"
});
setFocusedGroupIndex(index);
}}
/>
) : null}
<ListItemWrapper
key={resolvedItem.item.id}
item={resolvedItem.item}
data={resolvedItem.data}
context={context}
group={group}
compact={compact}
/>
</>
);
context={{
items,
group,
refresh,
focusedGroupIndex,
selectItems: setSelectedItems,
scrollToIndex: listRef.current?.scrollToIndex,
focusGroup: setFocusedGroupIndex,
context,
compact
}}
renderItem={ItemRenderer}
/>
</FlexScrollContainer>
</>
@@ -227,6 +181,85 @@ function ListContainer(props: ListContainerProps) {
}
export default ListContainer;
type ListContext = {
items: VirtualizedGrouping<Item>;
group: GroupingKey | undefined;
refresh: () => void;
focusedGroupIndex: number;
selectItems: (items: any) => void;
scrollToIndex?: (
index: number,
options?: ScrollToOptions | undefined
) => void;
focusGroup: (index: number) => void;
context?: Context;
compact?: boolean;
};
function ItemRenderer({
index,
context
}: {
index: number;
context: ListContext;
}) {
const {
items,
group,
refresh,
focusedGroupIndex,
focusGroup,
selectItems,
scrollToIndex,
context: itemContext,
compact
} = context;
const resolvedItem = useResolvedItem({ index, items });
if (!resolvedItem) return <div style={{ height: 50, width: "100%" }}></div>;
return (
<>
{resolvedItem.group && group ? (
<GroupHeader
groupingKey={group}
refresh={refresh}
title={resolvedItem.group.title}
isFocused={index === focusedGroupIndex}
index={index}
onSelectGroup={() => {
let endIndex;
for (let i = index + 1; i < items.ids.length; ++i) {
if (typeof items.ids[i] === "object") {
endIndex = i;
break;
}
}
selectItems([
...selectionStore.get().selectedItems,
...items.ids.slice(index, endIndex || items.ids.length)
]);
}}
groups={async () => (items.groups ? items.groups() : [])}
onJump={(index) => {
scrollToIndex?.(index, {
align: "center",
behavior: "auto"
});
focusGroup(index);
}}
/>
) : null}
<ListItemWrapper
key={resolvedItem.item.id}
item={resolvedItem.item}
data={resolvedItem.data}
context={itemContext}
group={group}
compact={compact}
/>
</>
);
}
/**
* Scroll the element at the specified index into view and
* wait until it renders into the DOM. This function keeps

View File

@@ -52,7 +52,7 @@ export function ResolvedItem<TItemType extends ItemType>(
[index, items]
);
if (result.status !== "fulfilled" || !result.value) return null;
if (result.status === "rejected" || !result.value) return null;
if (result.value.item.type !== type) return null;
return <>{children(result.value)}</>;
@@ -67,7 +67,7 @@ export function useResolvedItem(
[index, items]
);
if (result.status !== "fulfilled" || !result.value) return null;
if (result.status === "rejected" || !result.value) return null;
return result.value;
}

View File

@@ -21,7 +21,7 @@ import { Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
import { Box, BoxProps } from "@theme-ui/components";
import React, { useRef } from "react";
export type VirtualizedListProps<T> = {
export type VirtualizedListProps<T, C> = {
virtualizerRef?: React.MutableRefObject<
Virtualizer<Element, Element> | undefined
>;
@@ -30,13 +30,18 @@ export type VirtualizedListProps<T> = {
estimatedSize: number;
getItemKey: (index: number, items: T[]) => string;
scrollElement?: Element | null;
context?: C;
itemWrapperProps?: (item: T, index: number) => BoxProps;
renderItem: (props: { item: T; index: number }) => JSX.Element | null;
renderItem: (props: {
item: T;
index: number;
context: C;
}) => JSX.Element | null;
scrollMargin?: number;
itemGap?: number;
overscan?: number;
} & BoxProps;
export function VirtualizedList<T>(props: VirtualizedListProps<T>) {
export function VirtualizedList<T, C>(props: VirtualizedListProps<T, C>) {
const {
items,
getItemKey,
@@ -49,6 +54,7 @@ export function VirtualizedList<T>(props: VirtualizedListProps<T>) {
itemWrapperProps,
itemGap,
overscan = 5,
context,
...containerProps
} = props;
const containerRef = useRef<HTMLDivElement>(null);
@@ -99,7 +105,12 @@ export function VirtualizedList<T>(props: VirtualizedListProps<T>) {
}px)`
}}
>
<Item key={row.key} item={items[row.index]} index={row.index} />
<Item
key={row.key}
item={items[row.index]}
index={row.index}
context={context || ({} as C)}
/>
</Box>
))}
</Box>

View File

@@ -20,11 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { DependencyList, useEffect, useState } from "react";
export type PromiseResult<T> =
| PromisePendingResult
| PromisePendingResult<T>
| (PromiseSettledResult<T> & { refresh: () => void });
export interface PromisePendingResult {
export interface PromisePendingResult<T> {
status: "pending";
value?: T;
}
/**
@@ -62,7 +63,10 @@ export default function usePromise<T>(
useEffect(function effect() {
if (result.status !== "pending") {
setResult({ status: "pending" });
setResult((s) => ({
...s,
status: "pending"
}));
}
const controller = new AbortController();