Skip to content

Commit

Permalink
Merge pull request #141 from inokawa/scroll-restoration
Browse files Browse the repository at this point in the history
Implement scroll restoration
  • Loading branch information
inokawa authored Jul 27, 2023
2 parents 99ec4a5 + b1c50d3 commit 44078c6
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 132 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This project is a challenge to rethink virtualization. The goals are...
- **Zero configuration:** This library is designed to give the best performance without configuration. It also handles common hard things in the real world (dynamic size measurement, scroll position adjustment in bottom-up scrolling and imperative scrolling, etc).
- **Fast:** Scrolling without frame drop needs optimization in many aspects (reduce CPU usage, reduce GC, [reduce layout recalculation](https://gist.github.com/paulirish/5d52fb081b3570c81e3a), optimize with CSS, optimize for frameworks, etc). We are trying to combine the best of them.
- **Small:** Its bundle size should be small as much as possible to be friendly with modern web development. Currently each components are ~3kB gzipped and the total is [~4kB gzipped](https://bundlephobia.com/package/virtua).
- **Flexible:** Aiming to support many usecases - fixed size, dynamic size, horizontal scrolling, reverse scrolling, rtl direction, sticky, infinite scrolling, placeholder, scrollTo, dnd, table, and more. See [live demo](#demo).
- **Flexible:** Aiming to support many usecases - fixed size, dynamic size, horizontal scrolling, reverse scrolling, rtl direction, sticky, infinite scrolling, placeholder, scrollTo, scroll restoration, dnd, table, and more. See [live demo](#demo).
- **Framework agnostic (WIP):** Currently only for [React](https://react.dev/) but we could support [Vue](https://vuejs.org/), [Svelte](https://svelte.dev/), [Solid](https://www.solidjs.com/), [Angular](https://angular.io/), [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) and more in the future.

## Demo
Expand Down Expand Up @@ -192,6 +192,7 @@ It reduces rerender during scrolling with [caching element instances](https://gi
| Reverse scroll in iOS Safari |||||||
| Infinite scroll ||| 🟠 (needs [react-window-infinite-loader](https://github.com/bvaughn/react-window-infinite-loader)) | 🟠 (needs [InfiniteLoader](https://github.com/bvaughn/react-virtualized/blob/master/docs/InfiniteLoader.md)) |||
| Bi-directional infinite scroll |||||||
| Scroll restoration || ✅ (getState) |||||
| Smooth scroll |||||||
| RTL direction |||||||
| SSR support |||||||
Expand Down
11 changes: 9 additions & 2 deletions src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
hasUnmeasuredItemsInRange,
estimateDefaultItemSize,
} from "./cache";
import type { Writeable } from "./types";
import type { CacheSnapshot, Writeable } from "./types";
import { abs, exists, max, min } from "./utils";

type ItemJump = Readonly<[sizeDiff: number, index: number]>;
Expand Down Expand Up @@ -60,6 +60,7 @@ export const UPDATE_SIZE = 0b010;
export const UPDATE_JUMP = 0b100;

export type VirtualStore = {
_getCache(): CacheSnapshot;
_getRange(): ItemsRange;
_isUnmeasuredItem(index: number): boolean;
_hasUnmeasuredItemsInRange(startIndex: number): boolean;
Expand All @@ -86,6 +87,7 @@ export const createVirtualStore = (
initialItemCount: number = 0,
isReverse: boolean,
onScrollStateChange: (scrolling: boolean) => void,
cacheSnapshot?: CacheSnapshot,
onScrollOffsetChange?: (offset: number) => void
): VirtualStore => {
const shouldAutoEstimateItemSize = !itemSize;
Expand All @@ -94,7 +96,9 @@ export const createVirtualStore = (
let scrollOffset = 0;
let jumpCount = 0;
let jump: ScrollJump = 0;
let cache = resetCache(elementsCount, initialItemSize);
let cache =
(cacheSnapshot as unknown as Cache | undefined) ??
resetCache(elementsCount, initialItemSize);
let _scrollDirection: ScrollDirection = SCROLL_IDLE;
let _resized = false;
let _prevRange: ItemsRange = [0, initialItemCount];
Expand Down Expand Up @@ -124,6 +128,9 @@ export const createVirtualStore = (
};

return {
_getCache() {
return JSON.parse(JSON.stringify(cache)) as unknown as CacheSnapshot;
},
_getRange() {
const [prevStartIndex, prevEndIndex] = _prevRange;
const prevOffset = computeStartOffset(
Expand Down
10 changes: 10 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ export type DeepReadonly<T> = {
export type Writeable<T> = {
-readonly [key in keyof T]: Writeable<T[key]>;
};

declare const cacheSymbol: unique symbol;
/**
* Serializable cache snapshot.
*
* **This is not intended to be modified by users. And it is not guaranteed to work if you pass it to the different version ot this package.**
*/
export interface CacheSnapshot {
[cacheSymbol]: never;
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type { CacheSnapshot } from "./core/types";
export { VList } from "./react/VList";
export type { VListProps, VListHandle, ScrollMode } from "./react/VList";
export { VGrid } from "./react/VGrid";
Expand All @@ -8,7 +9,7 @@ export type {
CustomCellComponentProps,
} from "./react/VGrid";
export { WVList } from "./react/WVList";
export type { WVListProps } from "./react/WVList";
export type { WVListProps, WVListHandle } from "./react/WVList";
export type {
WindowComponentAttributes,
CustomWindowComponent,
Expand Down
14 changes: 14 additions & 0 deletions src/react/VList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Window as DefaultWindow,
} from "./Window";
import { CustomItemComponent, ListItem } from "./ListItem";
import { CacheSnapshot } from "../core/types";

export type ScrollMode = "reverse" | "rtl";

Expand All @@ -35,6 +36,10 @@ type CustomItemComponentOrElement =
* Methods of {@link VList}.
*/
export interface VListHandle {
/**
* Get current {@link CacheSnapshot}.
*/
readonly cache: CacheSnapshot;
/**
* Get current scrollTop or scrollLeft.
*/
Expand Down Expand Up @@ -99,6 +104,10 @@ export interface VListProps extends WindowComponentAttributes {
* - `rtl`: You have to set this mode if you use this component under `direction: rtl` style.
*/
mode?: ScrollMode;
/**
* You can restore cache by passing a {@link CacheSnapshot} on mount. This is useful when you want to restore scroll position after navigation. The snapshot can be obtained from {@link VListHandle.cache}.
*/
cache?: CacheSnapshot;
/**
* Customized element type for scrollable element. This element will get {@link CustomWindowComponentProps} as props.
* @defaultValue {@link Window}
Expand Down Expand Up @@ -149,6 +158,7 @@ export const VList = forwardRef<VListHandle, VListProps>(
initialItemCount,
horizontal: horizontalProp,
mode,
cache,
element: Window = DefaultWindow,
itemElement = "div",
onScroll: onScrollProp,
Expand Down Expand Up @@ -180,6 +190,7 @@ export const VList = forwardRef<VListHandle, VListProps>(
onScrollStop[refKey] && onScrollStop[refKey]();
}
},
cache,
(offset) => {
onScroll[refKey] && onScroll[refKey](offset);
}
Expand Down Expand Up @@ -232,6 +243,9 @@ export const VList = forwardRef<VListHandle, VListProps>(
ref,
() => {
return {
get cache() {
return store._getCache();
},
get scrollOffset() {
return store._getScrollOffset();
},
Expand Down
Loading

0 comments on commit 44078c6

Please sign in to comment.