Showing preview only (1,306K chars total). Download the full file or copy to clipboard to get everything.
Repository: bvaughn/react-window
Branch: main
Commit: 367baf743357
Files: 262
Total size: 1.2 MB
Directory structure:
gitextract_f4nhhmgs/
├── .github/
│ └── workflows/
│ ├── eslint.yml
│ ├── pending-changes.yml
│ ├── playwright.yml
│ ├── prettier.yml
│ ├── typescript.yml
│ └── vitest.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .prettierignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── eslint.config.js
├── index.css
├── index.html
├── index.tsx
├── integrations/
│ ├── next/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── app/
│ │ │ ├── decoder/
│ │ │ │ └── [encoded]/
│ │ │ │ ├── Decoder.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── grid/
│ │ │ │ ├── components/
│ │ │ │ │ └── CellComponent.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── list/
│ │ │ │ ├── components/
│ │ │ │ │ └── RowComponent.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── list-dynamic/
│ │ │ │ ├── components/
│ │ │ │ │ ├── List.tsx
│ │ │ │ │ └── RowComponent.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── page.tsx
│ │ │ └── tailwind.css
│ │ ├── eslint.config.mjs
│ │ ├── next-env.d.ts
│ │ ├── next.config.ts
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ └── tsconfig.json
│ ├── tests/
│ │ ├── package.json
│ │ ├── playwright.config.ts
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── AnimationFrameRowCellCounter.tsx
│ │ │ │ ├── DebugData.tsx
│ │ │ │ ├── Decoder.tsx
│ │ │ │ ├── EnvironmentMarker.tsx
│ │ │ │ ├── LayoutShiftDetecter.tsx
│ │ │ │ └── RowComponent.tsx
│ │ │ ├── index.ts
│ │ │ └── utils/
│ │ │ └── serializer/
│ │ │ ├── decode.ts
│ │ │ ├── encode.ts
│ │ │ └── types.ts
│ │ └── tests/
│ │ └── layout-shift.spec.tsx
│ ├── vike/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── pages/
│ │ │ ├── +Head.tsx
│ │ │ ├── +Layout.tsx
│ │ │ ├── +config.ts
│ │ │ ├── Layout.css
│ │ │ ├── _error/
│ │ │ │ └── +Page.tsx
│ │ │ ├── decoder/
│ │ │ │ ├── +Page.tsx
│ │ │ │ └── +route.ts
│ │ │ ├── grid/
│ │ │ │ ├── +Page.tsx
│ │ │ │ └── CellComponent.tsx
│ │ │ ├── index/
│ │ │ │ └── +Page.tsx
│ │ │ ├── list/
│ │ │ │ ├── +Page.tsx
│ │ │ │ └── RowComponent.tsx
│ │ │ ├── list-dynamic/
│ │ │ │ ├── +Page.tsx
│ │ │ │ └── RowComponent.tsx
│ │ │ └── tailwind.css
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ └── vite/
│ ├── README.md
│ ├── eslint.config.js
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── main.tsx
│ │ ├── routes/
│ │ │ ├── Decoder.tsx
│ │ │ ├── Grid.tsx
│ │ │ ├── Home.tsx
│ │ │ └── List.tsx
│ │ ├── tailwind.css
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── lib/
│ ├── components/
│ │ ├── grid/
│ │ │ ├── Grid.test.tsx
│ │ │ ├── Grid.tsx
│ │ │ ├── types.ts
│ │ │ ├── useGridCallbackRef.ts
│ │ │ └── useGridRef.ts
│ │ └── list/
│ │ ├── List.test.tsx
│ │ ├── List.tsx
│ │ ├── isDynamicRowHeight.ts
│ │ ├── types.ts
│ │ ├── useDynamicRowHeight.test.ts
│ │ ├── useDynamicRowHeight.ts
│ │ ├── useListCallbackRef.ts
│ │ └── useListRef.ts
│ ├── core/
│ │ ├── createCachedBounds.test.ts
│ │ ├── createCachedBounds.ts
│ │ ├── getEstimatedSize.test.ts
│ │ ├── getEstimatedSize.ts
│ │ ├── getOffsetForIndex.test.ts
│ │ ├── getOffsetForIndex.ts
│ │ ├── getStartStopIndices.test.ts
│ │ ├── getStartStopIndices.ts
│ │ ├── types.ts
│ │ ├── useCachedBounds.test.ts
│ │ ├── useCachedBounds.ts
│ │ ├── useIsRtl.ts
│ │ ├── useItemSize.ts
│ │ ├── useVirtualizer.test.ts
│ │ └── useVirtualizer.ts
│ ├── hooks/
│ │ ├── useIsomorphicLayoutEffect.ts
│ │ ├── useMemoizedObject.test.ts
│ │ ├── useMemoizedObject.ts
│ │ ├── useResizeObserver.test.ts
│ │ ├── useResizeObserver.ts
│ │ ├── useStableCallback.test.tsx
│ │ └── useStableCallback.ts
│ ├── index.ts
│ ├── types.ts
│ └── utils/
│ ├── adjustScrollOffsetForRtl.ts
│ ├── areArraysEqual.ts
│ ├── arePropsEqual.ts
│ ├── assert.ts
│ ├── colors/
│ │ ├── getContrastColor.ts
│ │ └── stringToColor.ts
│ ├── debug.ts
│ ├── getRTLOffsetType.ts
│ ├── getScrollbarSize.ts
│ ├── isRtl.ts
│ ├── parseNumericStyleValue.test.ts
│ ├── parseNumericStyleValue.ts
│ ├── shallowCompare.test.ts
│ ├── shallowCompare.ts
│ └── test/
│ ├── mockResizeObserver.ts
│ └── mockScrollTo.ts
├── package.json
├── pnpm-workspace.yaml
├── postcss.config.js
├── prettier.config.js
├── public/
│ ├── data/
│ │ ├── addresses.json
│ │ ├── contacts.json
│ │ ├── lorem.json
│ │ └── names.json
│ ├── generated/
│ │ ├── docs/
│ │ │ ├── Grid.json
│ │ │ ├── GridImperativeAPI.json
│ │ │ ├── List.json
│ │ │ └── ListImperativeAPI.json
│ │ ├── examples/
│ │ │ ├── BasicRow.json
│ │ │ ├── CellComponent.json
│ │ │ ├── CellComponentAriaRoles.json
│ │ │ ├── FixedHeightList.json
│ │ │ ├── FixedHeightRowComponent.json
│ │ │ ├── FlexboxLayout.json
│ │ │ ├── Grid.json
│ │ │ ├── GridAriaRoles.json
│ │ │ ├── HorizontalList.json
│ │ │ ├── HorizontalListCellRenderer.json
│ │ │ ├── ImageRow.json
│ │ │ ├── Images.json
│ │ │ ├── ListAriaRoles.json
│ │ │ ├── ListDynamicRowHeights.json
│ │ │ ├── ListRowDynamicRowHeights.json
│ │ │ ├── ListVariableRowHeights.json
│ │ │ ├── ListWithStickyRows.json
│ │ │ ├── RefComposition.json
│ │ │ ├── RowComponentAriaRoles.json
│ │ │ ├── RtlGrid.json
│ │ │ ├── ScrollingIndicator.json
│ │ │ ├── TableAriaAttributes.json
│ │ │ ├── TableAriaOverrideProps.json
│ │ │ ├── columnWidth.json
│ │ │ ├── gridRefClickEventHandler.json
│ │ │ ├── listRefClickEventHandler.json
│ │ │ ├── rowHeight.json
│ │ │ ├── shared.json
│ │ │ ├── useGridCallbackRef.json
│ │ │ ├── useGridRef.json
│ │ │ ├── useGridRefImport.json
│ │ │ ├── useListCallbackRef.json
│ │ │ ├── useListRef.json
│ │ │ └── useListRefImport.json
│ │ ├── search-index.json
│ │ └── search-records.json
│ └── robots.txt
├── scripts/
│ ├── compile-docs.ts
│ ├── compile-examples.ts
│ ├── compile-search-index.ts
│ └── compress-og-image.ts
├── src/
│ ├── App.tsx
│ ├── components/
│ │ ├── ContinueLink.tsx
│ │ ├── Link.tsx
│ │ └── NavLink.tsx
│ ├── constants.ts
│ ├── hooks/
│ │ └── useLocalStorage.ts
│ ├── routes/
│ │ ├── HowDoesItWorkRoute.tsx
│ │ ├── PlatformRequirementsRoute.tsx
│ │ ├── ScratchpadRoute.tsx
│ │ ├── examples/
│ │ │ ├── BasicRow.tsx
│ │ │ ├── RefComposition.tsx
│ │ │ └── ScrollingIndicator.tsx
│ │ ├── grid/
│ │ │ ├── AriaRolesRoute.tsx
│ │ │ ├── HorizontalListsRoute.tsx
│ │ │ ├── ImperativeHandleRoute.tsx
│ │ │ ├── PropsRoute.tsx
│ │ │ ├── RTLGridsRoute.tsx
│ │ │ ├── RenderingGridRoute.tsx
│ │ │ ├── ScrollToCellRoute.tsx
│ │ │ ├── examples/
│ │ │ │ ├── CellComponent.tsx
│ │ │ │ ├── CellComponentAriaRoles.tsx
│ │ │ │ ├── Grid.tsx
│ │ │ │ ├── GridAriaRoles.html
│ │ │ │ ├── HorizontalList.tsx
│ │ │ │ ├── HorizontalListCellRenderer.tsx
│ │ │ │ ├── RtlGrid.tsx
│ │ │ │ ├── columnWidth.ts
│ │ │ │ ├── gridRefClickEventHandler.ts
│ │ │ │ ├── shared.ts
│ │ │ │ ├── useGridCallbackRef.tsx
│ │ │ │ ├── useGridRef.tsx
│ │ │ │ └── useGridRefImport.ts
│ │ │ └── hooks/
│ │ │ ├── useContacts.ts
│ │ │ └── useEmails.ts
│ │ ├── list/
│ │ │ ├── AriaRolesRoute.tsx
│ │ │ ├── DynamicRowHeightsRoute.tsx
│ │ │ ├── FixedRowHeightsRoute.tsx
│ │ │ ├── ImagesRoute.tsx
│ │ │ ├── ImperativeApiRoute.tsx
│ │ │ ├── PropsRoute.tsx
│ │ │ ├── ScrollToRowRoute.tsx
│ │ │ ├── StickyRowsRoute.tsx
│ │ │ ├── VariableRowHeightsRoute.tsx
│ │ │ ├── examples/
│ │ │ │ ├── FixedHeightList.tsx
│ │ │ │ ├── FixedHeightRowComponent.tsx
│ │ │ │ ├── ImageRow.tsx
│ │ │ │ ├── Images.tsx
│ │ │ │ ├── ListAriaRoles.html
│ │ │ │ ├── ListDynamicRowHeights.tsx
│ │ │ │ ├── ListRowDynamicRowHeights.tsx
│ │ │ │ ├── ListVariableRowHeights.tsx
│ │ │ │ ├── ListWithStickyRows.tsx
│ │ │ │ ├── RowComponentAriaRoles.tsx
│ │ │ │ ├── listRefClickEventHandler.ts
│ │ │ │ ├── rowHeight.ts
│ │ │ │ ├── useListCallbackRef.tsx
│ │ │ │ ├── useListRef.tsx
│ │ │ │ └── useListRefImport.ts
│ │ │ └── hooks/
│ │ │ ├── useCitiesByState.ts
│ │ │ └── useLorem.ts
│ │ └── tables/
│ │ ├── AriaRolesRoute.tsx
│ │ ├── TabularDataRoute.tsx
│ │ ├── examples/
│ │ │ ├── FlexboxLayout.tsx
│ │ │ ├── TableAriaAttributes.html
│ │ │ └── TableAriaOverrideProps.tsx
│ │ └── hooks/
│ │ └── useAddresses.ts
│ ├── routes.ts
│ └── vite-env.d.ts
├── temp.TODO.md
├── tsconfig.json
├── vercel.json
├── vite.config.ts
├── vitest.config.ts
└── vitest.setup.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/eslint.yml
================================================
name: "ESLint"
on: [pull_request]
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2
with:
version: 10
- name: Install dependencies
run: pnpm install --frozen-lockfile --recursive
- name: Run ESLint
run: pnpm lint
================================================
FILE: .github/workflows/pending-changes.yml
================================================
name: "Pending changes"
on: [pull_request]
jobs:
pending-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2
with:
version: 10
- name: Install dependencies
run: pnpm install --frozen-lockfile --recursive
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true"
- name: Setup Google Chrome
uses: browser-actions/setup-chrome@v2
id: setup-chrome
with:
install-dependencies: true
- name: Testing
run: pnpm dev & pnpm run compile:search-index
env:
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
- name: Diff
run: git diff
================================================
FILE: .github/workflows/playwright.yml
================================================
name: "Playwright Tests"
on: [pull_request]
jobs:
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2
with:
version: 10
- name: Install dependencies
run: pnpm install --frozen-lockfile --recursive
- name: Cache Playwright browser binaries
id: playwright-cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright # Default Linux path
key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright browsers and dependencies (on cache miss)
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm e2e:install
- name: Build library
run: pnpm prerelease
- name: Run Playwright tests
run: pnpm dev:integrations & pnpm e2e:test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: integrations/tests/test-results/
retention-days: 14
================================================
FILE: .github/workflows/prettier.yml
================================================
name: "Prettier"
on: [pull_request]
jobs:
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2
with:
version: 10
- name: Install dependencies
run: pnpm install --frozen-lockfile --recursive
- name: Run Prettier
run: pnpm run prettier:ci
================================================
FILE: .github/workflows/typescript.yml
================================================
name: "TypeScript"
on: [pull_request]
jobs:
typescript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2
with:
version: 10
- name: Install dependencies
run: pnpm install --frozen-lockfile --recursive
- name: Build NPM package
run: pnpm build
- name: Run TypeScript
run: pnpm tsc
================================================
FILE: .github/workflows/vitest.yml
================================================
name: "Vitest"
on: [pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: pnpm/action-setup@v2
with:
version: 10
- name: Install dependencies
run: pnpm install --frozen-lockfile --recursive
- name: Build NPM packages
run: pnpm run build
- name: Run tests
run: pnpm run test:ci
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
/dist
/docs
*.local
integrations/tests/test-results
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: .husky/pre-commit
================================================
pnpm exec lint-staged
================================================
FILE: .prettierignore
================================================
/dist
/docs
/generated
/public
README.md
integrations/next/.next
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## 2.2.7
- Fixed a problem with project logo not displaying correctly in the README for the Firefox browser.
## 2.2.6
- `useDynamicRowHeight` should not instantiate `ResizeObserver` when server-rendering
## 2.2.5
- Use `defaultHeight`/`defaultWidth` prop to server render initial set of rows/cells
- Adjust TypeScript return type for `rowComponent`/`cellComponent` to work around a `ReactNode` vs `ReactElement` mismatch caused by #875
## 2.2.4
- Update README docs
## 2.2.3
- Update TS Doc comments for `List` and `Grid` imperative methods to specify when a method throws.
- Throw a `RangeError` (instead of a regular `Error`) if an invalid index is passed to one of the imperative scroll-to methods.
## 2.2.2
The return type of `List` and `Grid` components is explicitly annotated as `ReactElement`. The return type of `rowComponent` and `cellComponent` changed from `ReactNode` to `ReactElement`. This was done to fix TypeScript warnings for React versions 18.0 - 18.2. (See issue #875)
## 2.2.1
- Fix possible scroll-jump scenario with `useDynamicRowHeight`
## 2.2.0
- Support for dynamic row heights via new `useDynamicRowHeight` hook.
```tsx
const rowHeight = useDynamicRowHeight({
defaultRowHeight: 50
});
return <List rowHeight={rowHeight} {...rest} />;
```
- Smaller NPM bundle; (docs are no longer included as part of the bundle due to the added size)
## 2.1.2
Prevent `ResizeObserver` API from being called at all if an explicit `List` height (or `Grid` width and height) is provided.
## 2.1.1
Grids with only one row no longer incorrectly set cell height to 100%.
## 2.1.0
Improved ARIA support:
- Add better default ARIA attributes for outer `HTMLDivElement`
- Add optional `ariaAttributes` prop to row and cell renderers to simplify better ARIA attributes for user-rendered cells
- Remove intermediate `HTMLDivElement` from `List` and `Grid`
- This may enable more/better custom CSS styling
- This may also enable adding an optional `children` prop to `List` and `Grid` for e.g. overlays/tooltips
- Add optional `tagName` prop; defaults to `"div"` but can be changed to e.g. `"ul"`
```tsx
// Example of how to use new `ariaAttributes` prop
function RowComponent({
ariaAttributes,
index,
style,
...rest
}: RowComponentProps<object>) {
return (
<div style={style} {...ariaAttributes}>
...
</div>
);
}
```
Added optional `children` prop to better support edge cases like sticky rows.
Minor changes to `onRowsRendered` and `onCellsRendered` callbacks to make it easier to differentiate between _visible_ items and items rendered due to overscan settings. These methods will now receive two params– the first for _visible_ rows and the second for _all_ rows (including overscan), e.g.:
```ts
function onRowsRendered(
visibleRows: {
startIndex: number;
stopIndex: number;
},
allRows: {
startIndex: number;
stopIndex: number;
}
): void {
// ...
}
function onCellsRendered(
visibleCells: {
columnStartIndex: number;
columnStopIndex: number;
rowStartIndex: number;
rowStopIndex: number;
},
allCells: {
columnStartIndex: number;
columnStopIndex: number;
rowStartIndex: number;
rowStopIndex: number;
}
): void {
// ...
}
```
## 2.0.2
Fixed edge-case bug with `Grid` imperative API `scrollToCell` method and "smooth" scrolling behavior.
## 2.0.1
- Remove ARIA `role` attribute from `List` and `Grid`. This resulted in potentially invalid configurations (e.g. a ARIA _list_ should contain at least one _listitem_ but that was not enforced by this library). Users of this library should specify the `role` attribute that makes the most sense to them [based on mdn guidelines](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/list_role#best_practices). For example:
```tsx
<List
role="list"
rowComponent={RowComponent}
rowCount={names.length}
rowHeight={25}
rowProps={{ names }}
/>;
function RowComponent({ index, style, ...rest }: RowComponentProps<object>) {
return (
<div role="listitem" style={style}>
...
</div>
);
}
```
# 2.0.0
Version 2 is a major rewrite that offers the following benefits:
- More ergonomic props API
- Automatic memoization of row/cell renderers and props/context
- Automatically sizing for `List` and `Grid` (no more need for `AutoSizer`)
- Native TypeScript support (no more need for `@types/react-window`)
- Smaller bundle size
## Upgrade path
This section contains a couple of examples for common upgrade paths. Please refer to the [documentation](https://react-window.vercel.app/) for more information.
### Migrating `FixedSizeList`
#### Before
```tsx
import AutoSizer from "react-virtualized-auto-sizer";
import { FixedSizeList, type ListChildComponentProps } from "react-window";
function Example({ names }: { names: string[] }) {
const itemData = useMemo<ItemData>(() => ({ names }), [names]);
return (
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
children={Row}
height={height}
itemCount={names.length}
itemData={itemData}
itemSize={25}
width={width}
/>
)}
</AutoSizer>
);
}
function Row({
data,
index,
style
}: ListChildComponentProps<{
names: string[];
}>) {
const { names } = data;
const name = names[index];
return <div style={style}>{name}</div>;
}
```
#### After
```tsx
import { List, type RowComponentProps } from "react-window";
function Example({ names }: { names: string[] }) {
// You don't need to useMemo for rowProps;
// List will automatically memoize them
return (
<List
rowComponent={RowComponent}
rowCount={names.length}
rowHeight={25}
rowProps={{ names }}
/>
);
}
function RowComponent({
index,
names,
style
}: RowComponentProps<{
names: string[];
}>) {
const name = names[index];
return <div style={style}>{name}</div>;
}
```
### Migrating `VariableSizedList`
#### Before
```tsx
import AutoSizer from "react-virtualized-auto-sizer";
import { VariableSizeList, type ListChildComponentProps } from "react-window";
function Example({ items }: { items: Item[] }) {
const itemData = useMemo<ItemData>(() => ({ items }), [items]);
const itemSize = useCallback(
(index: number) => {
const item = itemData.items[index];
return item.type === "header" ? 40 : 20;
},
[itemData]
);
return (
<AutoSizer>
{({ height, width }) => (
<VariableSizeList
children={Row}
height={height}
itemCount={items.length}
itemData={itemData}
itemSize={itemSize}
width={width}
/>
)}
</AutoSizer>
);
}
function itemSize();
function Row({
data,
index,
style
}: ListChildComponentProps<{
items: Item[];
}>) {
const { items } = data;
const item = items[index];
return <div style={style}>{item.label}</div>;
}
```
#### After
```tsx
import { List, type RowComponentProps } from "react-window";
type RowProps = {
items: Item[];
};
function Example({ items }: { items: Item[] }) {
// You don't need to useMemo for rowProps;
// List will automatically memoize them
return (
<List
rowComponent={RowComponent}
rowCount={items.length}
rowHeight={rowHeight}
rowProps={{ items }}
/>
);
}
// The rowHeight method also receives the extra props,
// so it can be defined at the module level
function rowHeight(index: number, { item }: RowProps) {
return item.type === "header" ? 40 : 20;
}
function RowComponent({ index, items, style }: RowComponentProps<RowProps>) {
const item = items[index];
return <div style={style}>{item.label}</div>;
}
```
### Migrating `FixedSizeGrid`
#### Before
```tsx
import AutoSizer from "react-virtualized-auto-sizer";
import { FixedSizeGrid, type GridChildComponentProps } from "react-window";
function Example({ data }: { data: Data[] }) {
const itemData = useMemo<ItemData>(() => ({ data }), [data]);
return (
<AutoSizer>
{({ height, width }) => (
<FixedSizeGrid
children={Cell}
columnCount={data[0]?.length ?? 0}
columnWidth={100}
height={height}
itemData={itemData}
rowCount={data.length}
rowHeight={35}
width={width}
/>
)}
</AutoSizer>
);
}
function Cell({
columnIndex,
data,
rowIndex,
style
}: GridChildComponentProps<{
names: string[];
}>) {
const { data } = data;
const datum = data[index];
return <div style={style}>...</div>;
}
```
#### After
```tsx
import { FixedSizeGrid, type GridChildComponentProps } from "react-window";
function Example({ data }: { data: Data[] }) {
// You don't need to useMemo for cellProps;
// Grid will automatically memoize them
return (
<Grid
cellComponent={Cell}
cellProps={{ data }}
columnCount={data[0]?.length ?? 0}
columnWidth={75}
rowCount={data.length}
rowHeight={25}
/>
);
}
function Cell({
columnIndex,
data,
rowIndex,
style
}: CellComponentProps<{
data: Data[];
}>) {
const datum = data[rowIndex][columnIndex];
return <div style={style}>...</div>;
}
```
### Migrating `VariableSizeGrid`
#### Before
```tsx
import AutoSizer from "react-virtualized-auto-sizer";
import { VariableSizeGrid, type GridChildComponentProps } from "react-window";
function Example({ data }: { data: Data[] }) {
const itemData = useMemo<ItemData>(() => ({ data }), [data]);
const columnWidth = useCallback(
(columnIndex: number) => {
// ...
},
[itemData]
);
const rowHeight = useCallback(
(rowIndex: number) => {
// ...
},
[itemData]
);
return (
<AutoSizer>
{({ height, width }) => (
<VariableSizeGrid
children={Cell}
columnCount={data[0]?.length ?? 0}
columnWidth={columnWidth}
height={height}
itemData={itemData}
rowCount={data.length}
rowHeight={rowHeight}
width={width}
/>
)}
</AutoSizer>
);
}
function Cell({
columnIndex,
data,
rowIndex,
style
}: GridChildComponentProps<{
names: string[];
}>) {
const { data } = data;
const datum = data[index];
return <div style={style}>...</div>;
}
```
#### After
```tsx
import { FixedSizeGrid, type GridChildComponentProps } from "react-window";
type CellProps = {
data: Data[];
};
function Example({ data }: { data: Data[] }) {
// You don't need to useMemo for cellProps;
// Grid will automatically memoize them
return (
<Grid
cellComponent={Cell}
cellProps={{ data }}
columnCount={data[0]?.length ?? 0}
columnWidth={columnWidth}
rowCount={data.length}
rowHeight={rowHeight}
/>
);
}
// The columnWidth method also receives the extra props,
// so it can be defined at the module level
function columnWidth(columnIndex: number, { data }: CellProps) {
// ...
}
// The rowHeight method also receives the extra props,
// so it can be defined at the module level
function rowHeight(rowIndex: number, { data }: CellProps) {
// ...
}
function Cell({
columnIndex,
data,
rowIndex,
style
}: CellComponentProps<CellProps>) {
const datum = data[rowIndex][columnIndex];
return <div style={style}>...</div>;
}
```
### ⚠️ Version 2 requirements
The following requirements are new in version 2 and may be reasons to consider _not_ upgrading:
- Peer dependencies now require React version 18 or newer
- `ResizeObserver` primitive (or polyfill) is required _unless_ explicit pixel dimensions are provided via `style` prop; (see documentation for more)
## 1.8.11
- Dependencies updated to include React 19
## 1.8.10
- Fix scrollDirection when direction is RTL (#690)
## 1.8.9
- Readme changes
## 1.8.8
- 🐛 `scrollToItem` accounts for scrollbar size in the uncommon case where a List component has scrolling in the non-dominant direction (e.g. a "vertical" layout list also scrolls horizontally).
## 1.8.7
- ✨ Updated peer dependencies to include React v18.
## 1.8.6
- ✨ Updated peer dependencies to include React v17.
## 1.8.5
- ✨ Added UMD (dev and prod) build - ([emmanueltouzery](https://github.com/emmanueltouzery) - [#281](https://github.com/bvaughn/react-window/pull/281))
## 1.8.4
- 🐛 Fixed size list and grid components now accurately report `visibleStopIndex` in `onItemsRendered`. (Previously this value was incorrectly reported as one index higher.) - ([justingrant](https://github.com/justingrant) - [#274](https://github.com/bvaughn/react-window/pull/274))
- 🐛 Fixed size list and grid components `scrollToItem` "center" mode when the item being scrolled to is near the viewport edge. - ([justingrant](https://github.com/justingrant) - [#274](https://github.com/bvaughn/react-window/pull/274))
## 1.8.3
- 🐛 Edge case bug-fix for `scrollToItem` when scrollbars are present ([MarkFalconbridge](https://github.com/MarkFalconbridge) - [#267](https://github.com/bvaughn/react-window/pull/267))
- 🐛 Fixed RTL scroll offsets for non-Chromium Edge ([MarkFalconbridge](https://github.com/MarkFalconbridge) - [#268](https://github.com/bvaughn/react-window/pull/268))
- 🐛 Flow types improved ([TrySound](https://github.com/TrySound) - [#260](https://github.com/bvaughn/react-window/pull/260))
## 1.8.2
- ✨ Deprecated grid props `overscanColumnsCount` and `overscanRowsCount` props in favor of more consistently named `overscanColumnCount` and `overscanRowCount`. ([nihgwu](https://github.com/nihgwu) - [#229](https://github.com/bvaughn/react-window/pull/229))
- 🐛 Fixed shaky elastic scroll problems present in iOS Safari. [#244](https://github.com/bvaughn/react-window/issues/244)
- 🐛 Fixed RTL edge case bugs and broken scroll-to-item behavior. [#159](https://github.com/bvaughn/react-window/issues/159)
- 🐛 Fixed broken synchronized scrolling for RTL lists/grids. [#198](https://github.com/bvaughn/react-window/issues/198)
## 1.8.1
- 🐛 Replaced an incorrect empty-string value for `pointer-events` with `undefined` ([oliviertassinari](https://github.com/oliviertassinari) - [#210](https://github.com/bvaughn/react-window/pull/210))
## 1.8.0
- 🎉 Added new "smart" align option for grid and list scroll-to-item methods ([gaearon](https://github.com/gaearon) - [#209](https://github.com/bvaughn/react-window/pull/209))
## 1.7.2
- 🐛 Add guards to avoid invalid scroll offsets when `scrollTo()` is called with a negative offset or when `scrollToItem` is called with invalid indices (negative or too large).
## 1.7.1
- 🐛 Fix SSR regression introduced in 1.7.0 - ([Betree](https://github.com/Betree) - [#185](https://github.com/bvaughn/react-window/pull/185))
## 1.7.0
- 🎉 Grid `scrollToItem` supports optional `rowIndex` and `columnIndex` params ([jgoz](https://github.com/jgoz) - [#174](https://github.com/bvaughn/react-window/pull/174))
- DEV mode checks for `WeakSet` support before using it to avoid requiring a polyfill for IE11 - ([jgoz](https://github.com/jgoz) - [#167](https://github.com/bvaughn/react-window/pull/167))
## 1.6.2
- 🐛 Bugfix for RTL when scrolling back towards the beginning (right) of the list.
## 1.6.1
- 🐛 Bugfix to account for differences between Chrome and non-Chrome browsers with regard to RTL and "scroll" events.
## 1.6.0
- 🎉 RTL support added for lists and grids. Special thanks to [davidgarsan](https://github.com/davidgarsan) for his support. - [#156](https://github.com/bvaughn/react-window/pull/156)
- 🐛 Grid `scrollToItem` methods take scrollbar size into account when aligning items - [#153](https://github.com/bvaughn/react-window/issues/153)
## 1.5.2
- 🐛 Edge case bug fix for `VariableSizeList` and `VariableSizeGrid` when the number of items decreases while a scroll is in progress. - ([iamsolankiamit](https://github.com/iamsolankiamit) - [#138](https://github.com/bvaughn/react-window/pull/138))
## 1.5.1
- 🐛 Updated `getDerivedState` Flow annotations to address a warning in a newer version of Flow.
## 1.5.0
- 🎉 Added advanced memoization helpers methods `areEqual` and `shouldComponentUpdate` for item renderers. - [#114](https://github.com/bvaughn/react-window/issues/114)
## 1.4.0
- 🎉 List and Grid components now "overscan" (pre-render) in both directions when scrolling is not active. When scrolling is in progress, cells are only pre-rendered in the direction being scrolled. This change has been made in an effort to reduce visible flicker when scrolling starts without adding additional overhead during scroll (which is the most performance sensitive time).
- 🎉 Grid components now support separate `overscanColumnsCount` and `overscanRowsCount` props. Legacy `overscanCount` prop will continue to work, but with a deprecation warning in DEV mode.
- 🐛 Replaced `setTimeout` with `requestAnimationFrame` based timer, to avoid starvation issue for `isScrolling` reset. - [#106](https://github.com/bvaughn/react-window/issues/106)
- 🎉 Renamed List and Grid `innerTagName` and `outerTagName` props to `innerElementType` and `outerElementType` to formalize support for attaching arbitrary props (e.g. test ids) to List and Grid inner and outer DOM elements. Legacy `innerTagName` and `outerTagName` props will continue to work, but with a deprecation warning in DEV mode.
- 🐛 List re-renders items if `direction` prop changes. - [#104](https://github.com/bvaughn/react-window/issues/104)
## 1.3.1
- 🎉 Pass `itemData` value to custom `itemKey` callbacks when present - [#90](https://github.com/bvaughn/react-window/issues/90))
## 1.3.0
- (Skipped)
## 1.2.4
- 🐛 Added Flow annotations to memoized methods to avoid a Flow warning for newer versions of Flow
## 1.2.3
- 🐛 Relaxed `children` validation checks. They were too strict and didn't support new React APIs like `memo`.
## 1.2.2
- 🐛 Improved Flow types for class component item renderers - ([nicholas-l](https://github.com/nicholas-l) - [#77](https://github.com/bvaughn/react-window/pull/77))
## 1.2.1
- 🎉 Improved Flow types to include optional `itemData` parameter. ([TrySound](https://github.com/TrySound) - [#66](https://github.com/bvaughn/react-window/pull/66))
- 🐛 `VariableSizeList` and `VariableSizeGrid` no longer call size getter functions with invalid index when item count is zero.
## 1.2.0
- 🎉 Flow types added to NPM package. ([TrySound](https://github.com/TrySound) - [#40](https://github.com/bvaughn/react-window/pull/40))
- 🎉 Relaxed grid `scrollTo` method to make `scrollLeft` and `scrollTop` params _optional_ (so you can only update one axis if desired). - [#63](https://github.com/bvaughn/react-window/pull/63))
- 🐛 Fixed invalid `this` pointer in `VariableSizeGrid` that broke the `resetAfter*` methods - [#58](https://github.com/bvaughn/react-window/pull/58))
- Upgraded to babel 7 and used shared runtime helpers to reduce package size slightly. ([TrySound](https://github.com/TrySound) - [#48](https://github.com/bvaughn/react-window/pull/48))
- Remove `overflow:hidden` from inner container ([souporserious](https://github.com/souporserious) - [#56](https://github.com/bvaughn/react-window/pull/56))
## 1.1.2
- 🐛 Fixed edge case `scrollToItem` bug that caused lists/grids with very few items to have negative scroll offsets.
## 1.1.1
- 🐛 `FixedSizeGrid` and `FixedSizeList` automatically clear style cache when item size props change.
## 1.1.0
- 🎉 Use explicit `constructor` and `super` to generate cleaner component code. ([Andarist](https://github.com/Andarist) - [#26](https://github.com/bvaughn/react-window/pull/26))
- 🎉 Add optional `shouldForceUpdate` param reset-index methods to specify `forceUpdate` behavior. ([nihgwu](https://github.com/nihgwu) - [#32](https://github.com/bvaughn/react-window/pull/32))
## 1.0.3
- 🐛 Avoid unnecessary scrollbars for lists (e.g. no horizontal scrollbar for a vertical list) unless content requires them.
## 1.0.2
- 🎉 Enable Babel `annotate-pure-calls` option so that classes compiled by "transform-es2015-classes" are annotated with `#__PURE__`. This enables [UglifyJS to remove them if they are not referenced](https://github.com/mishoo/UglifyJS2/pull/1448), improving dead code elimination in application code. ([Andarist](https://github.com/Andarist) - [#20](https://github.com/bvaughn/react-window/pull/20))
- 🎉 Update "rollup-plugin-peer-deps-external" and use new `includeDependencies` flag so that the "memoize-one" dependency does not get inlined into the Rollup bundle. ([Andarist](https://github.com/Andarist) - [#19](https://github.com/bvaughn/react-window/pull/19))
- 🎉 Enable [Babel "loose" mode](https://babeljs.io/docs/en/babel-preset-env#loose) to reduce package size (-8%). ([Andarist](https://github.com/Andarist) - [#18](https://github.com/bvaughn/react-window/pull/18))
## 1.0.1
Updated `README.md` file to remove `@alpha` tag from NPM installation instructions.
# 1.0.0
Initial release of library. Includes the following components:
- `FixedSizeGrid`
- `FixedSizeList`
- `VariableSizeGrid`
- `VariableSizeList`
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Thanks for your interest in contributing to this project!
Here are a couple of guidelines to keep in mind before opening a Pull Request:
- Please open a GitHub issue for discussion _before_ submitting any significant changes to this API (including new features or functionality).
- Please don't submit code that has been written by code-generation tools such as Copilot or Claude. (There's nothing wrong with these tools, but I'd prefer them not be a part of this project.)
## Local development
To get started:
```sh
pnpm install
```
### Running the documentation site locally
The documentation site is a great place to test pending changes. It runs on localhost port 3000 and can be started by running:
```sh
pnpm dev
```
### Running tests locally
To run unit tests locally:
```sh
pnpm test
```
### Updating assets
Before subtmitting, also make sure to update generated docs/examples:
```
pnpm compile
pnpm prettier
pnpm lint
```
> [!NOTE]
> If you forget this step, CI will remind you!
================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)
Copyright (c) 2018 Brian Vaughn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<img src="https://react-window.vercel.app/og.png" alt="react-window logo" width="400" height="210" />
`react-window` is a component library that helps render large lists of data quickly and without the performance problems that often go along with rendering a lot of data. It's used in a lot of places, from React DevTools to the Replay browser.
## Support
If you like this project there are several ways to support it:
- [Become a GitHub sponsor](https://github.com/sponsors/bvaughn/)
- [Become an Open Collective sponsor](https://opencollective.com/react-window#sponsor)
- or [buy me a coffee](http://givebrian.coffee/)
The following wonderful companies and individuals have sponsored react-window:
<a href="https://opencollective.com/react-window/sponsor/0/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/0/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/1/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/1/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/2/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/2/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/3/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/3/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/4/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/4/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/5/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/5/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/6/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/6/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/7/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/7/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/8/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/8/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/9/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/9/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/10/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/10/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/11/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/11/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/12/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/12/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/13/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/13/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/14/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/14/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/15/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/15/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/16/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/16/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/17/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/17/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/18/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/18/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/19/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/19/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/20/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/20/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/21/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/21/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/22/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/22/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/23/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/23/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/24/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/24/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/25/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/25/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/26/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/26/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/27/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/27/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/28/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/28/avatar.svg"></a> <a href="https://opencollective.com/react-window/sponsor/29/website" target="_blank"><img src="https://opencollective.com/react-window/sponsor/29/avatar.svg"></a>
<a href="https://opencollective.com/react-window/backer/0/website" target="_blank"><img src="https://opencollective.com/react-window/backer/0/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/1/website" target="_blank"><img src="https://opencollective.com/react-window/backer/1/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/2/website" target="_blank"><img src="https://opencollective.com/react-window/backer/2/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/3/website" target="_blank"><img src="https://opencollective.com/react-window/backer/3/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/4/website" target="_blank"><img src="https://opencollective.com/react-window/backer/4/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/5/website" target="_blank"><img src="https://opencollective.com/react-window/backer/5/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/6/website" target="_blank"><img src="https://opencollective.com/react-window/backer/6/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/7/website" target="_blank"><img src="https://opencollective.com/react-window/backer/7/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/8/website" target="_blank"><img src="https://opencollective.com/react-window/backer/8/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/9/website" target="_blank"><img src="https://opencollective.com/react-window/backer/9/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/10/website" target="_blank"><img src="https://opencollective.com/react-window/backer/10/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/11/website" target="_blank"><img src="https://opencollective.com/react-window/backer/11/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/12/website" target="_blank"><img src="https://opencollective.com/react-window/backer/12/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/13/website" target="_blank"><img src="https://opencollective.com/react-window/backer/13/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/14/website" target="_blank"><img src="https://opencollective.com/react-window/backer/14/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/15/website" target="_blank"><img src="https://opencollective.com/react-window/backer/15/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/16/website" target="_blank"><img src="https://opencollective.com/react-window/backer/16/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/17/website" target="_blank"><img src="https://opencollective.com/react-window/backer/17/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/18/website" target="_blank"><img src="https://opencollective.com/react-window/backer/18/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/19/website" target="_blank"><img src="https://opencollective.com/react-window/backer/19/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/20/website" target="_blank"><img src="https://opencollective.com/react-window/backer/20/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/21/website" target="_blank"><img src="https://opencollective.com/react-window/backer/21/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/22/website" target="_blank"><img src="https://opencollective.com/react-window/backer/22/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/23/website" target="_blank"><img src="https://opencollective.com/react-window/backer/23/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/24/website" target="_blank"><img src="https://opencollective.com/react-window/backer/24/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/25/website" target="_blank"><img src="https://opencollective.com/react-window/backer/25/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/26/website" target="_blank"><img src="https://opencollective.com/react-window/backer/26/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/27/website" target="_blank"><img src="https://opencollective.com/react-window/backer/27/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/28/website" target="_blank"><img src="https://opencollective.com/react-window/backer/28/avatar.svg"></a> <a href="https://opencollective.com/react-window/backer/29/website" target="_blank"><img src="https://opencollective.com/react-window/backer/29/avatar.svg"></a>
## Installation
Begin by installing the library from NPM:
```sh
npm install react-window
```
## TypeScript types
TypeScript definitions are included within the published `dist` folder
## FAQs
Frequently asked questions can be found [here](https://react-window.vercel.app/common-questions).
## Documentation
Documentation for this project is available at [react-window.vercel.app](https://react-window.vercel.app/); version 1.x documentation can be found at [react-window-v1.vercel.app](https://react-window-v1.vercel.app/).
### List
<!-- List:description:begin -->
Renders data with many rows.
<!-- List:description:end -->
#### Required props
<!-- List:required-props:begin -->
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>rowComponent</td>
<td><p>React component responsible for rendering a row.</p>
<p>This component will receive an <code>index</code> and <code>style</code> prop by default.
Additionally it will receive prop values passed to <code>rowProps</code>.</p>
<p>ℹ️ The prop types for this component are exported as <code>RowComponentProps</code></p>
</td>
</tr>
<tr>
<td>rowCount</td>
<td><p>Number of items to be rendered in the list.</p>
</td>
</tr>
<tr>
<td>rowHeight</td>
<td><p>Row height; the following formats are supported:</p>
<ul>
<li>number of pixels (number)</li>
<li>percentage of the grid's current height (string)</li>
<li>function that returns the row height (in pixels) given an index and <code>cellProps</code></li>
<li>dynamic row height cache returned by the <code>useDynamicRowHeight</code> hook</li>
</ul>
<p>⚠️ Dynamic row heights are not as efficient as predetermined sizes.
It's recommended to provide your own height values if they can be determined ahead of time.</p>
</td>
</tr>
<tr>
<td>rowProps</td>
<td><p>Additional props to be passed to the row-rendering component.
List will automatically re-render rows when values in this object change.</p>
<p>⚠️ This object must not contain <code>ariaAttributes</code>, <code>index</code>, or <code>style</code> props.</p>
</td>
</tr>
</tbody>
</table>
<!-- List:required-props:end -->
#### Optional props
<!-- List:optional-props:begin -->
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>className</td>
<td><p>CSS class name.</p>
</td>
</tr>
<tr>
<td>style</td>
<td><p>Optional CSS properties.
The list of rows will fill the height defined by this style.</p>
</td>
</tr>
<tr>
<td>children</td>
<td><p>Additional content to be rendered within the list (above cells).
This property can be used to render things like overlays or tooltips.</p>
</td>
</tr>
<tr>
<td>defaultHeight</td>
<td><p>Default height of list for initial render.
This value is important for server rendering.</p>
</td>
</tr>
<tr>
<td>listRef</td>
<td><p>Ref used to interact with this component's imperative API.</p>
<p>This API has imperative methods for scrolling and a getter for the outermost DOM element.</p>
<p>ℹ️ The <code>useListRef</code> and <code>useListCallbackRef</code> hooks are exported for convenience use in TypeScript projects.</p>
</td>
</tr>
<tr>
<td>onResize</td>
<td><p>Callback notified when the List's outermost HTMLElement resizes.
This may be used to (re)scroll a row into view.</p>
</td>
</tr>
<tr>
<td>onRowsRendered</td>
<td><p>Callback notified when the range of visible rows changes.</p>
</td>
</tr>
<tr>
<td>overscanCount</td>
<td><p>How many additional rows to render outside of the visible area.
This can reduce visual flickering near the edges of a list when scrolling.</p>
</td>
</tr>
<tr>
<td>tagName</td>
<td><p>Can be used to override the root HTML element rendered by the List component.
The default value is "div", meaning that List renders an HTMLDivElement as its root.</p>
<p>⚠️ In most use cases the default ARIA roles are sufficient and this prop is not needed.</p>
</td>
</tr>
</tbody>
</table>
<!-- List:optional-props:end -->
### Grid
<!-- Grid:description:begin -->
Renders data with many rows and columns.
ℹ️ Unlike `List` rows, `Grid` cell sizes must be known ahead of time.
Either static sizes or something that can be derived (from the data in `CellProps`) without rendering.
<!-- Grid:description:end -->
#### Required props
<!-- Grid:required-props:begin -->
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>cellComponent</td>
<td><p>React component responsible for rendering a cell.</p>
<p>This component will receive an <code>index</code> and <code>style</code> prop by default.
Additionally it will receive prop values passed to <code>cellProps</code>.</p>
<p>ℹ️ The prop types for this component are exported as <code>CellComponentProps</code></p>
</td>
</tr>
<tr>
<td>cellProps</td>
<td><p>Additional props to be passed to the cell-rendering component.
Grid will automatically re-render cells when values in this object change.</p>
<p>⚠️ This object must not contain <code>ariaAttributes</code>, <code>columnIndex</code>, <code>rowIndex</code>, or <code>style</code> props.</p>
</td>
</tr>
<tr>
<td>columnCount</td>
<td><p>Number of columns to be rendered in the grid.</p>
</td>
</tr>
<tr>
<td>columnWidth</td>
<td><p>Column width; the following formats are supported:</p>
<ul>
<li>number of pixels (number)</li>
<li>percentage of the grid's current width (string)</li>
<li>function that returns the column width (in pixels) given an index and <code>cellProps</code></li>
</ul>
</td>
</tr>
<tr>
<td>rowCount</td>
<td><p>Number of rows to be rendered in the grid.</p>
</td>
</tr>
<tr>
<td>rowHeight</td>
<td><p>Row height; the following formats are supported:</p>
<ul>
<li>number of pixels (number)</li>
<li>percentage of the grid's current height (string)</li>
<li>function that returns the row height (in pixels) given an index and <code>cellProps</code></li>
</ul>
</td>
</tr>
</tbody>
</table>
<!-- Grid:required-props:end -->
#### Optional props
<!-- Grid:optional-props:begin -->
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>className</td>
<td><p>CSS class name.</p>
</td>
</tr>
<tr>
<td>dir</td>
<td><p>Indicates the directionality of grid cells.</p>
<p>ℹ️ See HTML <code>dir</code> <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir">global attribute</a> for more information.</p>
</td>
</tr>
<tr>
<td>style</td>
<td><p>Optional CSS properties.
The grid of cells will fill the height and width defined by this style.</p>
</td>
</tr>
<tr>
<td>children</td>
<td><p>Additional content to be rendered within the grid (above cells).
This property can be used to render things like overlays or tooltips.</p>
</td>
</tr>
<tr>
<td>defaultHeight</td>
<td><p>Default height of grid for initial render.
This value is important for server rendering.</p>
</td>
</tr>
<tr>
<td>defaultWidth</td>
<td><p>Default width of grid for initial render.
This value is important for server rendering.</p>
</td>
</tr>
<tr>
<td>gridRef</td>
<td><p>Imperative Grid API.</p>
<p>ℹ️ The <code>useGridRef</code> and <code>useGridCallbackRef</code> hooks are exported for convenience use in TypeScript projects.</p>
</td>
</tr>
<tr>
<td>onCellsRendered</td>
<td><p>Callback notified when the range of rendered cells changes.</p>
</td>
</tr>
<tr>
<td>onResize</td>
<td><p>Callback notified when the Grid's outermost HTMLElement resizes.
This may be used to (re)scroll a cell into view.</p>
</td>
</tr>
<tr>
<td>overscanCount</td>
<td><p>How many additional rows/columns to render outside of the visible area.
This can reduce visual flickering near the edges of a grid when scrolling.</p>
</td>
</tr>
<tr>
<td>tagName</td>
<td><p>Can be used to override the root HTML element rendered by the List component.
The default value is "div", meaning that List renders an HTMLDivElement as its root.</p>
<p>⚠️ In most use cases the default ARIA roles are sufficient and this prop is not needed.</p>
</td>
</tr>
</tbody>
</table>
<!-- Grid:optional-props:end -->
================================================
FILE: eslint.config.js
================================================
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { globalIgnores } from "eslint/config";
export default tseslint.config([
globalIgnores([
"dist",
"docs",
"public/generated",
"integrations/next/.next"
]),
{
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs["recommended-latest"],
reactRefresh.configs.vite
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser
}
}
]);
================================================
FILE: index.css
================================================
@source "node_modules/react-lib-tools";
@import "tailwindcss";
@import "react-lib-tools/styles.css";
@theme {
--color-background-gradient-1: var(--color-emerald-400);
--color-background-gradient-2: var(--color-indigo-500);
--color-background-gradient-3: var(--color-emerald-400);
--color-common-question-header: var(--color-indigo-100);
--color-focus-1: var(--color-teal-300);
--color-focus-2: var(--color-teal-400);
--color-focus-3: var(--color-teal-500);
}
================================================
FILE: index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>react-window | render everything</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="react-window" />
<meta property="og:title" content="react-window: render everything" />
<meta
name="description"
content="Documentation for the react-window NPM package"
/>
<meta property="og:url" content="https://react-window.vercel.app/" />
<meta
property="og:image"
content="https://react-window.vercel.app/og.png"
/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>
================================================
FILE: index.tsx
================================================
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./src/App.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);
================================================
FILE: integrations/next/.gitignore
================================================
.next
================================================
FILE: integrations/next/README.md
================================================
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
================================================
FILE: integrations/next/app/decoder/[encoded]/Decoder.tsx
================================================
"use client";
import { useState } from "react";
import { Decoder as DecoderExternal } from "../../../../tests";
export function Decoder({
encoded: encodedProp,
searchParams: searchParamsMap
}: {
encoded: string;
searchParams: { [key: string]: string | undefined } | undefined;
}) {
const [encoded] = useState(() => decodeURIComponent(encodedProp));
const [searchParams] = useState(() => {
const params = new URLSearchParams();
for (const key in searchParamsMap) {
params.set(key, searchParamsMap[key] ?? "");
}
return params;
});
return <DecoderExternal encoded={encoded} searchParams={searchParams} />;
}
================================================
FILE: integrations/next/app/decoder/[encoded]/page.tsx
================================================
import { Decoder } from "./Decoder";
export default async function Page({
params,
searchParams: searchParamsPromise
}: {
params: Promise<{ encoded: string }>;
searchParams?: { [key: string]: string | undefined };
}) {
const { encoded } = await params;
const searchParams = await searchParamsPromise;
return <Decoder encoded={encoded} searchParams={searchParams} />;
}
================================================
FILE: integrations/next/app/grid/components/CellComponent.tsx
================================================
"use client";
import { type CellComponentProps } from "react-window";
export function CellComponent({
ariaAttributes,
columnIndex,
rowIndex,
style
}: CellComponentProps<object>) {
return (
<div className="flex items-center gap-1" style={style} {...ariaAttributes}>
Cell {rowIndex}, {columnIndex}
</div>
);
}
================================================
FILE: integrations/next/app/grid/page.tsx
================================================
import { Grid } from "react-window";
import {
AnimationFrameRowCellCounter,
EnvironmentMarker,
LayoutShiftDetecter
} from "../../../tests";
import { CellComponent } from "./components/CellComponent";
export default async function Home() {
return (
<div className="p-2 flex flex-col gap-2">
<EnvironmentMarker>NextJS (server components)</EnvironmentMarker>
<AnimationFrameRowCellCounter />
<LayoutShiftDetecter />
<Grid
cellComponent={CellComponent}
cellProps={{}}
className="h-[250px] w-[250px]"
columnCount={10}
columnWidth={100}
defaultHeight={250}
defaultWidth={250}
overscanCount={0}
rowCount={100}
rowHeight={25}
/>
</div>
);
}
================================================
FILE: integrations/next/app/layout.tsx
================================================
/* eslint-disable react-refresh/only-export-components */
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./tailwind.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"]
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"]
});
export const metadata: Metadata = {
title: "[Next] react-window integration",
description: "Generated by create next app"
};
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
================================================
FILE: integrations/next/app/list/components/RowComponent.tsx
================================================
"use client";
import { type RowComponentProps } from "react-window";
export function RowComponent({
ariaAttributes,
index,
style
}: RowComponentProps<object>) {
return (
<div className="flex items-center gap-1" style={style} {...ariaAttributes}>
Row {index}
</div>
);
}
================================================
FILE: integrations/next/app/list/page.tsx
================================================
import { List } from "react-window";
import {
AnimationFrameRowCellCounter,
EnvironmentMarker,
LayoutShiftDetecter
} from "../../../tests";
import { RowComponent } from "./components/RowComponent";
export default async function Home() {
return (
<div className="p-2 flex flex-col gap-2">
<EnvironmentMarker>NextJS (server components)</EnvironmentMarker>
<AnimationFrameRowCellCounter />
<LayoutShiftDetecter />
<List
className="h-[250px]"
defaultHeight={250}
overscanCount={0}
rowComponent={RowComponent}
rowCount={100}
rowHeight={25}
rowProps={{}}
/>
</div>
);
}
================================================
FILE: integrations/next/app/list-dynamic/components/List.tsx
================================================
"use client";
import { List as ListExternal, useDynamicRowHeight } from "react-window";
import { RowComponent } from "./RowComponent";
export function List() {
const rowHeight = useDynamicRowHeight({
defaultRowHeight: 30
});
return (
<ListExternal
className="h-[250px]"
defaultHeight={250}
overscanCount={0}
rowComponent={RowComponent}
rowCount={100}
rowHeight={rowHeight}
rowProps={{}}
/>
);
}
================================================
FILE: integrations/next/app/list-dynamic/components/RowComponent.tsx
================================================
"use client";
import { type RowComponentProps } from "react-window";
export function RowComponent({
ariaAttributes,
index,
style
}: RowComponentProps<object>) {
return (
<div className="flex items-center gap-1" style={style} {...ariaAttributes}>
Row {index}
</div>
);
}
================================================
FILE: integrations/next/app/list-dynamic/page.tsx
================================================
import {
AnimationFrameRowCellCounter,
EnvironmentMarker,
LayoutShiftDetecter
} from "../../../tests";
import { List } from "./components/List";
export default async function Home() {
return (
<div className="p-2 flex flex-col gap-2">
<EnvironmentMarker>NextJS (server components)</EnvironmentMarker>
<AnimationFrameRowCellCounter />
<LayoutShiftDetecter />
<List />
</div>
);
}
================================================
FILE: integrations/next/app/page.tsx
================================================
import Link from "next/link";
export default async function Home() {
return (
<div className="p-2 flex flex-col gap-2">
<Link href="/list">List</Link>
<Link href="/list-dynamic">List + useDynamicRowHeight</Link>
<Link href="/grid">Grid</Link>
</div>
);
}
================================================
FILE: integrations/next/app/tailwind.css
================================================
@source "../../tests";
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
================================================
FILE: integrations/next/eslint.config.mjs
================================================
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts"
])
]);
export default eslintConfig;
================================================
FILE: integrations/next/next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
================================================
FILE: integrations/next/next.config.ts
================================================
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
================================================
FILE: integrations/next/package.json
================================================
{
"name": "next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --port 3010",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "16.0.10",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-window": "workspace:*"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}
================================================
FILE: integrations/next/postcss.config.mjs
================================================
const config = {
plugins: {
"@tailwindcss/postcss": {}
}
};
export default config;
================================================
FILE: integrations/next/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"plugins": [
{
"name": "next"
}
],
"strictNullChecks": true
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts",
"**/*.ts",
"**/*.tsx"
],
"exclude": ["node_modules"],
"tsconfigRootDir": ""
}
================================================
FILE: integrations/tests/package.json
================================================
{
"name": "tests",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "src/index.ts",
"scripts": {
"test": "npx playwright test"
},
"dependencies": {
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-window": "workspace:*",
"react-router": "^7"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@playwright/test": "^1",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}
================================================
FILE: integrations/tests/playwright.config.ts
================================================
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
projects: [
{
name: "chromium",
timeout: 10_000,
use: {
...devices["Desktop Chrome"],
viewport: { width: 1000, height: 600 }
// Uncomment to visually debug
// headless: false,
// launchOptions: {
// slowMo: 500
// }
}
}
]
});
================================================
FILE: integrations/tests/src/components/AnimationFrameRowCellCounter.tsx
================================================
"use client";
import { useLayoutEffect, useRef } from "react";
export function AnimationFrameRowCellCounter() {
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const element = ref.current;
if (element) {
const id = requestAnimationFrame(() => {
const cellCount =
document.body.querySelectorAll('[role="gridcell"]').length;
const rowCount =
document.body.querySelectorAll('[role="listitem"]').length;
element.textContent = `${cellCount + rowCount}`;
});
return () => {
cancelAnimationFrame(id);
};
}
}, []);
return (
<div className="flex flex-row gap-1 text-xs text-green-400">
Rows/cells on mount: <div ref={ref} />
</div>
);
}
================================================
FILE: integrations/tests/src/components/DebugData.tsx
================================================
import { cn } from "react-lib-tools";
export function DebugData({ data }: { data: object }) {
return (
<pre
className={cn(
"p-2 resize-none rounded-md font-mono text-xs",
"border border-2 border-slate-800 focus:outline-none focus:border-sky-700"
)}
>
<code className="text-xs">{JSON.stringify(data, replacer, 2)}</code>
</pre>
);
}
function replacer(_key: string, value: unknown) {
if (typeof value === "number") {
return Math.round(value);
}
return value;
}
================================================
FILE: integrations/tests/src/components/Decoder.tsx
================================================
"use client";
import { useMemo } from "react";
import { Box } from "react-lib-tools";
import {} from "react-window";
import { decode } from "../utils/serializer/decode";
export function Decoder({
encoded
}: {
encoded: string;
searchParams: URLSearchParams;
}) {
// TODO const [state, setState] = useState(null);
const children = useMemo(() => {
if (!encoded) {
return null;
}
return decode(encoded);
}, [encoded]);
// Debugging
// console.group("Decoder");
// console.log(encoded);
// console.log(children);
// console.groupEnd();
return (
<Box direction="column" gap={2}>
<div>{children}</div>
<Box className="p-2 overflow-auto" direction="row" gap={2} wrap>
TODO
</Box>
</Box>
);
}
================================================
FILE: integrations/tests/src/components/EnvironmentMarker.tsx
================================================
export function EnvironmentMarker({ children }: { children: string }) {
const comment =
typeof window === "undefined"
? "<!-- SERVER MARKER -->"
: "<!-- CLIENT MARKER -->";
return (
<div
className="flex items-center gap-1 text-xs text-slate-300"
dangerouslySetInnerHTML={{
__html: `${comment} ${children}`
}}
/>
);
}
================================================
FILE: integrations/tests/src/components/LayoutShiftDetecter.tsx
================================================
"use client";
import { useEffect, useInsertionEffect, useState } from "react";
type PerformanceEntry = {
hadRecentInput: boolean;
sources: unknown[];
value: number;
};
export function LayoutShiftDetecter() {
const [state, setState] = useState<number | null>(null);
useInsertionEffect(() => {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries() as unknown as PerformanceEntry[]) {
if (!entry.hadRecentInput) {
setState(entry.value);
}
}
});
observer.observe({ type: "layout-shift", buffered: true });
}, []);
useEffect(() => {
const timeout = setTimeout(() => {
setState((prevState) => (prevState === null ? 0 : prevState));
}, 250);
return () => {
clearTimeout(timeout);
};
}, []);
switch (state) {
case null: {
return (
<div className="text-xs text-slate-500">Measuring layout shift ...</div>
);
}
case 0: {
return <div className="text-xs text-green-400">✅ No layout shift</div>;
}
default: {
return (
<div className="text-xs text-red-400">
❌ Layout shift detected: {state}
</div>
);
}
}
}
================================================
FILE: integrations/tests/src/components/RowComponent.tsx
================================================
import { type RowComponentProps } from "react-window";
export type RowComponentData = {
data: string[];
};
export function RowComponent({
index,
data,
style
}: RowComponentProps<RowComponentData>) {
return (
<div className="flex items-center px-1" style={style}>
{data[index]}
</div>
);
}
================================================
FILE: integrations/tests/src/index.ts
================================================
export { Decoder } from "./components/Decoder";
export { AnimationFrameRowCellCounter } from "./components/AnimationFrameRowCellCounter";
export { LayoutShiftDetecter } from "./components/LayoutShiftDetecter";
export { EnvironmentMarker } from "./components/EnvironmentMarker";
================================================
FILE: integrations/tests/src/utils/serializer/decode.ts
================================================
import { createElement, type ReactElement } from "react";
import { List, type ListProps, type RowComponentProps } from "react-window";
import {
RowComponent,
type RowComponentData
} from "../../../src/components/RowComponent";
import type {
EncodedElement,
EncodedListElement,
EncodedRowComponentElement,
EncodedTextElement,
TextProps
} from "./types";
let key = 0;
export function decode(stringified: string) {
const json = JSON.parse(stringified) as EncodedElement[];
return decodeChildren(json);
}
function decodeChildren(children: EncodedElement[]): ReactElement<unknown>[] {
const elements: ReactElement<unknown>[] = [];
children.forEach((current) => {
if (!current) {
return;
}
switch (current.type) {
case "List": {
elements.push(decodeList(current));
break;
}
case "RowComponent": {
elements.push(decodeRowComponent(current));
break;
}
case "Text": {
elements.push(decodeText(current));
break;
}
default: {
console.warn("Could not decode type:", current);
}
}
});
return elements;
}
function decodeList(
json: EncodedListElement
): ReactElement<ListProps<RowComponentData>> {
return createElement(List<RowComponentData>, {
key: ++key,
...json.props
});
}
function decodeRowComponent(
json: EncodedRowComponentElement
): ReactElement<RowComponentProps<RowComponentData>> {
return createElement(RowComponent, {
key: ++key,
...json.props
});
}
function decodeText(json: EncodedTextElement): ReactElement<TextProps> {
return createElement("div", {
key: ++key,
...json.props
});
}
================================================
FILE: integrations/tests/src/utils/serializer/encode.ts
================================================
import { type ReactElement } from "react";
import { List, type ListProps, type RowComponentProps } from "react-window";
import {
RowComponent,
type RowComponentData
} from "../../../src/components/RowComponent";
import type {
EncodedElement,
EncodedListElement,
EncodedRowComponentElement,
EncodedTextElement,
TextProps
} from "./types";
export function encode(element: ReactElement<unknown>) {
const json = encodeChildren([element]);
const stringified = JSON.stringify(json);
return encodeURIComponent(stringified);
}
function encodeChildren(children: ReactElement<unknown>[]): EncodedElement[] {
const elements: EncodedElement[] = [];
children.forEach((current) => {
if (!current) {
return;
}
switch (current.type) {
case List: {
elements.push(
encodeList(current as ReactElement<ListProps<RowComponentData>>)
);
break;
}
case RowComponent: {
elements.push(
encodeRowComponent(
current as ReactElement<RowComponentProps<RowComponentData>>
)
);
break;
}
default: {
if (typeof current === "object") {
const { children } = current.props as TextProps;
if (typeof children === "string") {
elements.push(encodeTextChild(current as ReactElement<TextProps>));
} else {
console.warn("Could not encode type:", current);
}
}
}
}
});
return elements;
}
function encodeList(
element: ReactElement<ListProps<RowComponentData>>
): EncodedListElement {
return {
props: element.props,
type: "List"
};
}
function encodeRowComponent(
element: ReactElement<RowComponentProps<RowComponentData>>
): EncodedRowComponentElement {
return {
props: element.props,
type: "RowComponent"
};
}
function encodeTextChild(element: ReactElement<TextProps>): EncodedTextElement {
return {
props: {
children: element.props.children,
className: element.props.className
},
type: "Text"
};
}
================================================
FILE: integrations/tests/src/utils/serializer/types.ts
================================================
import type { ListProps, RowComponentProps } from "react-window";
import type { RowComponentData } from "../../components/RowComponent";
export interface EncodedListElement {
props: ListProps<RowComponentData>;
type: "List";
}
export interface EncodedRowComponentElement {
props: RowComponentProps<RowComponentData>;
type: "RowComponent";
}
export type TextProps = {
children: string;
className?: string | undefined;
};
export interface EncodedTextElement {
props: TextProps;
type: "Text";
}
export type EncodedElement =
| EncodedListElement
| EncodedRowComponentElement
| EncodedTextElement;
================================================
FILE: integrations/tests/tests/layout-shift.spec.tsx
================================================
import { expect, test, type Page } from "@playwright/test";
// High level tests; more nuanced scenarios are covered by unit tests
test.describe("layout-shift", () => {
async function assertRenderedBy(page: Page, type: "client" | "server") {
const innerHTML = await page.evaluate(() => document.body.innerHTML);
expect(innerHTML).toContain(
type === "client" ? "<!-- CLIENT MARKER -->" : "<!-- SERVER MARKER -->"
);
}
test.describe("client-rendered apps", () => {
const HOST = "http://localhost:3012";
test("List", async ({ page }) => {
await page.goto(`${HOST}/list`);
await assertRenderedBy(page, "client");
await expect(page.getByText("Rows/cells on mount: 10")).toBeVisible();
await expect(page.getByText("No layout shift")).toBeVisible();
});
test("Grid", async ({ page }) => {
await page.goto(`${HOST}/grid`);
await assertRenderedBy(page, "client");
await expect(page.getByText("Rows/cells on mount: 30")).toBeVisible();
await expect(page.getByText("No layout shift")).toBeVisible();
});
});
test.describe("server-rendered apps", () => {
const HOST = "http://localhost:3011";
test("List", async ({ page }) => {
await page.goto(`${HOST}/list`);
await assertRenderedBy(page, "server");
await expect(page.getByText("Rows/cells on mount: 10")).toBeVisible();
await expect(page.getByText("No layout shift")).toBeVisible();
});
test("Grid", async ({ page }) => {
await page.goto(`${HOST}/grid`);
await assertRenderedBy(page, "server");
await expect(page.getByText("Rows/cells on mount: 30")).toBeVisible();
await expect(page.getByText("No layout shift")).toBeVisible();
});
});
test.describe("server component apps", () => {
const HOST = "http://localhost:3010";
test("List", async ({ page }) => {
await page.goto(`${HOST}/list`);
await assertRenderedBy(page, "server");
await expect(page.getByText("Rows/cells on mount: 10")).toBeVisible();
await expect(page.getByText("No layout shift")).toBeVisible();
});
test("Grid", async ({ page }) => {
await page.goto(`${HOST}/grid`);
await assertRenderedBy(page, "server");
await expect(page.getByText("Rows/cells on mount: 30")).toBeVisible();
await expect(page.getByText("No layout shift")).toBeVisible();
});
});
});
================================================
FILE: integrations/vike/README.md
================================================
Generated with [vike.dev/new](https://vike.dev/new) ([version 531](https://www.npmjs.com/package/create-vike/v/0.0.531)) using this command:
```sh
npm create vike@latest --- --react --tailwindcss
```
## Contents
- [Vike](#vike)
- [Plus files](#plus-files)
- [Routing](#routing)
- [SSR](#ssr)
- [HTML Streaming](#html-streaming)
## Vike
This app is ready to start. It's powered by [Vike](https://vike.dev) and [React](https://react.dev/learn).
### Plus files
[The + files are the interface](https://vike.dev/config) between Vike and your code.
- [`+config.ts`](https://vike.dev/settings) — Settings (e.g. `<title>`)
- [`+Page.tsx`](https://vike.dev/Page) — The `<Page>` component
- [`+data.ts`](https://vike.dev/data) — Fetching data (for your `<Page>` component)
- [`+Layout.tsx`](https://vike.dev/Layout) — The `<Layout>` component (wraps your `<Page>` components)
- [`+Head.tsx`](https://vike.dev/Head) - Sets `<head>` tags
- [`/pages/_error/+Page.tsx`](https://vike.dev/error-page) — The error page (rendered when an error occurs)
- [`+onPageTransitionStart.ts`](https://vike.dev/onPageTransitionStart) and `+onPageTransitionEnd.ts` — For page transition animations
### Routing
[Vike's built-in router](https://vike.dev/routing) lets you choose between:
- [Filesystem Routing](https://vike.dev/filesystem-routing) (the URL of a page is determined based on where its `+Page.jsx` file is located on the filesystem)
- [Route Strings](https://vike.dev/route-string)
- [Route Functions](https://vike.dev/route-function)
### SSR
SSR is enabled by default. You can [disable it](https://vike.dev/ssr) for all or specific pages.
### HTML Streaming
You can [enable/disable HTML streaming](https://vike.dev/stream) for all or specific pages.
================================================
FILE: integrations/vike/package.json
================================================
{
"scripts": {
"dev": "vike dev --port 3011",
"build": "vike build",
"preview": "vike build && vike preview",
"tsc": "tsc -b"
},
"dependencies": {
"vike": "^0.4.247",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-window": "workspace:*",
"vike-react": "^0.6.13"
},
"devDependencies": {
"typescript": "^5.9.3",
"vite": "^7.2.4",
"@vitejs/plugin-react": "^5.1.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.1.17",
"@tailwindcss/vite": "^4.1.17"
},
"type": "module"
}
================================================
FILE: integrations/vike/pages/+Head.tsx
================================================
// https://vike.dev/Head
export function Head() {
return null;
}
================================================
FILE: integrations/vike/pages/+Layout.tsx
================================================
import "./Layout.css";
import "./tailwind.css";
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
================================================
FILE: integrations/vike/pages/+config.ts
================================================
import type { Config } from "vike/types";
import vikeReact from "vike-react/config";
// Default config (can be overridden by pages)
// https://vike.dev/config
const config: Config = {
// https://vike.dev/head-tags
title: "[Vike] react-window integration",
description: "Test harness",
htmlAttributes: { class: "dark bg-black text-white" },
extends: [vikeReact]
};
export default config;
================================================
FILE: integrations/vike/pages/Layout.css
================================================
/* Links */
a {
text-decoration: none;
}
#sidebar a {
padding: 2px 10px;
margin-left: -10px;
}
#sidebar a.is-active {
background-color: #eee;
}
/* Reset */
body {
margin: 0;
font-family: sans-serif;
}
* {
box-sizing: border-box;
}
/* Page Transition Animation */
#page-content {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
body.page-transition #page-content {
opacity: 0;
}
================================================
FILE: integrations/vike/pages/_error/+Page.tsx
================================================
import { usePageContext } from "vike-react/usePageContext";
export default function Page() {
const { is404 } = usePageContext();
if (is404) {
return (
<>
<h1>Page Not Found</h1>
<p>This page could not be found.</p>
</>
);
}
return (
<>
<h1>Internal Error</h1>
<p>Something went wrong.</p>
</>
);
}
================================================
FILE: integrations/vike/pages/decoder/+Page.tsx
================================================
import { useState } from "react";
import { usePageContext } from "vike-react/usePageContext";
import { Decoder } from "../../../tests/src";
export default function Page() {
const { urlParsed } = usePageContext();
const [encoded] = useState(() => urlParsed.pathname.replace("/decoder/", ""));
const [searchParams] = useState(
() => new URLSearchParams(urlParsed.searchOriginal || "")
);
return <Decoder encoded={encoded} searchParams={searchParams} />;
}
================================================
FILE: integrations/vike/pages/decoder/+route.ts
================================================
export default "*";
================================================
FILE: integrations/vike/pages/grid/+Page.tsx
================================================
import { Grid } from "react-window";
import {
AnimationFrameRowCellCounter,
EnvironmentMarker,
LayoutShiftDetecter
} from "../../../tests";
import { CellComponent } from "./CellComponent";
export default function Page() {
return (
<div className="p-2 flex flex-col gap-2">
<EnvironmentMarker>Vike (server rendering)</EnvironmentMarker>
<AnimationFrameRowCellCounter />
<LayoutShiftDetecter />
<Grid
cellComponent={CellComponent}
cellProps={{}}
className="h-[250px] w-[250px]"
columnCount={10}
columnWidth={100}
defaultHeight={250}
defaultWidth={250}
overscanCount={0}
rowCount={100}
rowHeight={25}
/>
</div>
);
}
================================================
FILE: integrations/vike/pages/grid/CellComponent.tsx
================================================
import { type CellComponentProps } from "react-window";
export function CellComponent({
ariaAttributes,
columnIndex,
rowIndex,
style
}: CellComponentProps<object>) {
return (
<div className="flex items-center gap-1" style={style} {...ariaAttributes}>
Cell {rowIndex}, {columnIndex}
</div>
);
}
================================================
FILE: integrations/vike/pages/index/+Page.tsx
================================================
export default function Page() {
return (
<div className="p-2 flex flex-col gap-2">
<a href="/list">List</a>
<a href="/list-dynamic">List + useDynamicRowHeight</a>
<a href="/grid">Grid</a>
</div>
);
}
================================================
FILE: integrations/vike/pages/list/+Page.tsx
================================================
import { List } from "react-window";
import {
AnimationFrameRowCellCounter,
EnvironmentMarker,
LayoutShiftDetecter
} from "../../../tests";
import { RowComponent } from "./RowComponent";
export default function Page() {
return (
<div className="p-2 flex flex-col gap-2">
<EnvironmentMarker>Vike (server rendering)</EnvironmentMarker>
<AnimationFrameRowCellCounter />
<LayoutShiftDetecter />
<List
className="h-[250px]"
defaultHeight={250}
overscanCount={0}
rowComponent={RowComponent}
rowCount={100}
rowHeight={25}
rowProps={{}}
/>
</div>
);
}
================================================
FILE: integrations/vike/pages/list/RowComponent.tsx
================================================
import { type RowComponentProps } from "react-window";
export function RowComponent({
ariaAttributes,
index,
style
}: RowComponentProps<object>) {
return (
<div className="flex items-center gap-1" style={style} {...ariaAttributes}>
Row {index}
</div>
);
}
================================================
FILE: integrations/vike/pages/list-dynamic/+Page.tsx
================================================
import { List, useDynamicRowHeight } from "react-window";
import {
AnimationFrameRowCellCounter,
EnvironmentMarker,
LayoutShiftDetecter
} from "../../../tests";
import { RowComponent } from "./RowComponent";
export default function Page() {
const rowHeight = useDynamicRowHeight({
defaultRowHeight: 30
});
return (
<div className="p-2 flex flex-col gap-2">
<EnvironmentMarker>Vike (server rendering)</EnvironmentMarker>
<AnimationFrameRowCellCounter />
<LayoutShiftDetecter />
<List
className="h-[250px]"
defaultHeight={250}
overscanCount={0}
rowComponent={RowComponent}
rowCount={100}
rowHeight={rowHeight}
rowProps={{}}
/>
</div>
);
}
================================================
FILE: integrations/vike/pages/list-dynamic/RowComponent.tsx
================================================
import { type RowComponentProps } from "react-window";
export function RowComponent({
ariaAttributes,
index,
style
}: RowComponentProps<object>) {
return (
<div className="flex items-center gap-1" style={style} {...ariaAttributes}>
Row {index}
</div>
);
}
================================================
FILE: integrations/vike/pages/tailwind.css
================================================
@source "../../tests";
@import "tailwindcss";
@layer base {
h1 {
@apply mb-4 text-4xl font-bold tracking-tight text-gray-900;
}
ul {
@apply list-disc pl-6;
}
ol {
@apply list-decimal pl-6;
}
p {
@apply mb-2 mt-2;
}
a {
@apply text-blue-600 hover:text-pink-400 visited:text-blue-900;
}
}
================================================
FILE: integrations/vike/tsconfig.json
================================================
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["vite/client", "vike-react", "vike-react/usePageContext"],
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"exclude": ["dist"]
}
================================================
FILE: integrations/vike/vite.config.ts
================================================
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import vike from "vike/plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [vike(), react(), tailwindcss()]
});
================================================
FILE: integrations/vite/README.md
================================================
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked
],
languageOptions: {
// other options...
parserOptions: {
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname
}
}
});
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
"react-x": reactX,
"react-dom": reactDom
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs["recommended-typescript"].rules,
...reactDom.configs.recommended.rules
}
});
```
================================================
FILE: integrations/vite/eslint.config.js
================================================
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
tsconfigRootDir: import.meta.dirname
}
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh
},
rules: {
...reactHooks.configs.recommended.rules,
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true
}
],
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true }
]
}
}
);
================================================
FILE: integrations/vite/index.html
================================================
<!doctype html>
<html class="dark" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>[Vite] react-window integration</title>
</head>
<body class="bg-black text-white">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: integrations/vite/package.json
================================================
{
"name": "vite",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 3012",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-window": "workspace:*",
"react-router": "^7"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@tailwindcss/vite": "^4.1.17",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"tailwindcss": "^4.1.17",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}
================================================
FILE: integrations/vite/src/main.tsx
================================================
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router";
import { DecoderRoute } from "./routes/Decoder";
import { HomeRoute } from "./routes/Home";
import { GridRoute } from "./routes/Grid";
import { ListRoute } from "./routes/List";
import "./tailwind.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<HomeRoute />} />
<Route path="/list" element={<ListRoute />} />
<Route path="/grid" element={<GridRoute />} />
<Route path="/decoder/:encoded" element={<DecoderRoute />} />
</Routes>
</BrowserRouter>
</StrictMode>
);
================================================
FILE: integrations/vite/src/routes/Decoder.tsx
================================================
import { useParams, useSearchParams } from "react-router";
import { Decoder } from "../../../tests/src";
export function DecoderRoute() {
const { encoded } = useParams();
const [searchParams] = useSearchParams();
return <Decoder encoded={encoded ?? ""} searchParams={searchParams} />;
}
================================================
FILE: integrations/vite/src/routes/Grid.tsx
================================================
import { Grid, type CellComponentProps } from "react-window";
import {
AnimationFrameRowCellCounter,
EnvironmentMarker,
LayoutShiftDetecter
} from "../../../tests";
export function GridRoute() {
return (
<div className="p-2 flex flex-col gap-2">
<EnvironmentMarker>Vite (client rendering)</EnvironmentMarker>
<AnimationFrameRowCellCounter />
<LayoutShiftDetecter />
<Grid
cellComponent={CellComponent}
cellProps={{}}
className="h-[250px] w-[250px]"
columnCount={10}
columnWidth={100}
defaultHeight={250}
defaultWidth={250}
overscanCount={0}
rowCount={100}
rowHeight={25}
/>
</div>
);
}
function CellComponent({
ariaAttributes,
columnIndex,
rowIndex,
style
}: CellComponentProps<object>) {
return (
<div className="flex items-center gap-1" style={style} {...ariaAttributes}>
Cell {rowIndex}, {columnIndex}
</div>
);
}
================================================
FILE: integrations/vite/src/routes/Home.tsx
================================================
import { Link } from "react-router";
export function HomeRoute() {
return (
<div className="p-2 flex flex-col gap-2">
<Link to="/list">List</Link>
<Link to="/grid">Grid</Link>
</div>
);
}
================================================
FILE: integrations/vite/src/routes/List.tsx
================================================
import { List, type RowComponentProps } from "react-window";
import {
AnimationFrameRowCellCounter,
EnvironmentMarker,
LayoutShiftDetecter
} from "../../../tests";
export function ListRoute() {
return (
<div className="p-2 flex flex-col gap-2">
<EnvironmentMarker>Vite (client rendering)</EnvironmentMarker>
<AnimationFrameRowCellCounter />
<LayoutShiftDetecter />
<List
className="h-[250px]"
defaultHeight={250}
overscanCount={0}
rowComponent={RowComponent}
rowCount={100}
rowHeight={25}
rowProps={{}}
/>
</div>
);
}
function RowComponent({
ariaAttributes,
index,
style
}: RowComponentProps<object>) {
return (
<div className="flex items-center gap-2" style={style} {...ariaAttributes}>
Row {index}
</div>
);
}
================================================
FILE: integrations/vite/src/tailwind.css
================================================
@source "../../tests";
@import "tailwindcss";
@layer base {
h1 {
@apply mb-4 text-4xl font-bold tracking-tight text-gray-900;
}
ul {
@apply list-disc pl-6;
}
ol {
@apply list-decimal pl-6;
}
p {
@apply mb-2 mt-2;
}
a {
@apply text-blue-600 hover:text-pink-400 visited:text-blue-900;
}
}
#root {
height: 100vh;
}
================================================
FILE: integrations/vite/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />
================================================
FILE: integrations/vite/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
================================================
FILE: integrations/vite/vite.config.ts
================================================
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
cors: true
}
});
================================================
FILE: lib/components/grid/Grid.test.tsx
================================================
import { render, screen } from "@testing-library/react";
import { createRef, useLayoutEffect } from "react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { EMPTY_OBJECT } from "../../../src/constants";
import {
disableResizeObserverForCurrentTest,
setDefaultElementSize,
simulateUnsupportedEnvironmentForTest
} from "../../utils/test/mockResizeObserver";
import { Grid } from "./Grid";
import type { CellComponentProps, GridImperativeAPI } from "./types";
import { useGridCallbackRef } from "./useGridCallbackRef";
describe("Grid", () => {
let mountedCells: Map<string, CellComponentProps<object>> = new Map();
const CellComponent = vi.fn(function Cell(props: CellComponentProps<object>) {
const { ariaAttributes, columnIndex, rowIndex, style } = props;
const key = `${rowIndex},${columnIndex}`;
useLayoutEffect(() => {
mountedCells.set(key, props);
return () => {
mountedCells.delete(key);
};
});
return (
<div {...ariaAttributes} style={style}>
Cell {key}
</div>
);
});
beforeEach(() => {
CellComponent.mockReset();
setDefaultElementSize({ height: 40, width: 100 });
mountedCells = new Map();
});
test("should render an empty Grid", () => {
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={0}
columnWidth={25}
overscanCount={0}
rowCount={0}
rowHeight={20}
/>
);
const items = screen.queryAllByRole("gridcell");
expect(items).toHaveLength(0);
});
test("should render extra cells for overscan", () => {
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth={25}
overscanCount={2}
rowCount={100}
rowHeight={20}
/>
);
// 4 columns (+2) by 2 rows (+2)
const items = screen.queryAllByRole("gridcell");
expect(items).toHaveLength(24);
});
describe("cell sizes", () => {
test("type: number (px)", () => {
const { container } = render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth={25}
overscanCount={0}
rowCount={100}
rowHeight={20}
/>
);
// 4 columns by 2 rows
expect(container.querySelectorAll('[role="gridcell"]')).toHaveLength(8);
});
test("type: function (px)", () => {
const columnWidth = () => 50;
const rowHeight = () => 20;
const { container } = render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth={columnWidth}
overscanCount={0}
rowCount={100}
rowHeight={rowHeight}
/>
);
// 2 columns by 2 rows
expect(container.querySelectorAll('[role="gridcell"]')).toHaveLength(4);
});
test("type: string (%)", () => {
const { container } = render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth="25%"
overscanCount={0}
rowCount={100}
rowHeight="25%"
/>
);
// 4 columns by 4 rows
expect(container.querySelectorAll('[role="gridcell"]')).toHaveLength(16);
});
});
test("should pass cellProps to the cellComponent", () => {
render(
<Grid
cellComponent={CellComponent}
cellProps={{
foo: "abc",
bar: 123
}}
columnCount={100}
columnWidth="50%"
overscanCount={0}
rowCount={100}
rowHeight="25%"
/>
);
expect(mountedCells.size).toEqual(8);
expect(mountedCells.get("0,0")).toMatchObject({
foo: "abc",
bar: 123
});
});
test("should re-render items if cellComponent changes", () => {
const { rerender } = render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth="50%"
overscanCount={0}
rowCount={100}
rowHeight="25%"
/>
);
const NewCellComponent = vi.fn(() => <div />);
rerender(
<Grid
cellComponent={NewCellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth="50%"
overscanCount={0}
rowCount={100}
rowHeight="25%"
/>
);
expect(NewCellComponent).toHaveBeenCalled();
});
test("should re-render items if cell size changes", () => {
const { rerender } = render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth="50%"
overscanCount={0}
rowCount={100}
rowHeight="25%"
/>
);
expect(mountedCells).toHaveLength(8);
rerender(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth="50%"
overscanCount={0}
rowCount={100}
rowHeight="50%"
/>
);
expect(mountedCells).toHaveLength(4);
});
test("should re-render items if cellProps change", () => {
const { rerender } = render(
<Grid
cellComponent={CellComponent}
cellProps={{
foo: "abc"
}}
columnCount={100}
columnWidth="50%"
overscanCount={0}
rowCount={100}
rowHeight="50%"
/>
);
expect(mountedCells).toHaveLength(4);
expect(mountedCells.get("0,0")).toMatchObject({
foo: "abc"
});
rerender(
<Grid
cellComponent={CellComponent}
cellProps={{
bar: 123
}}
columnCount={100}
columnWidth="50%"
overscanCount={0}
rowCount={100}
rowHeight="50%"
/>
);
expect(mountedCells).toHaveLength(4);
expect(mountedCells.get("0,0")).toMatchObject({
bar: 123
});
});
test("should use default sizes for initial mount", () => {
// Mimic server rendering
disableResizeObserverForCurrentTest();
render(
<Grid
cellComponent={CellComponent}
cellProps={{
bar: 123
}}
defaultHeight={100}
defaultWidth={300}
columnCount={100}
columnWidth={75}
overscanCount={0}
rowCount={100}
rowHeight={50}
/>
);
const items = screen.queryAllByRole("gridcell");
expect(items).toHaveLength(8);
});
test("should call onCellsRendered", () => {
const onCellsRendered = vi.fn();
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth={25}
onCellsRendered={onCellsRendered}
overscanCount={2}
rowCount={100}
rowHeight={20}
/>
);
expect(onCellsRendered).toHaveBeenCalled();
expect(onCellsRendered).toHaveBeenLastCalledWith(
{
columnStartIndex: 0,
columnStopIndex: 3,
rowStartIndex: 0,
rowStopIndex: 1
},
{
columnStartIndex: 0,
columnStopIndex: 5,
rowStartIndex: 0,
rowStopIndex: 3
}
);
});
test("should support custom className and style props", () => {
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
className="foo"
columnCount={100}
columnWidth={25}
overscanCount={0}
rowCount={100}
rowHeight={20}
style={{
backgroundColor: "red"
}}
/>
);
const grid = screen.queryByRole("grid");
expect(grid).toHaveClass("foo");
expect(grid?.style.backgroundColor).toBe("red");
});
test("should spread HTML rest attributes", () => {
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth={25}
data-testid="foo"
overscanCount={2}
rowCount={100}
rowHeight={20}
/>
);
expect(screen.queryByTestId("foo")).toHaveRole("grid");
});
test("custom tagName and attributes", () => {
function CustomCellComponent({ style }: CellComponentProps<object>) {
return <span style={style}>Cell</span>;
}
const { container } = render(
<Grid
cellComponent={CustomCellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth={25}
overscanCount={0}
rowCount={100}
rowHeight={20}
tagName="main"
/>
);
expect(container.firstElementChild?.tagName).toBe("MAIN");
expect(container.querySelectorAll("SPAN")).toHaveLength(8);
});
test("children", () => {
const { container } = render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth={25}
overscanCount={0}
rowCount={100}
rowHeight={20}
>
<div id="custom">Overlay or tooltip</div>
</Grid>
);
expect(container.querySelector("#custom")).toHaveTextContent(
"Overlay or tooltip"
);
});
describe("imperative API", () => {
test("should return the root element", () => {
const gridRef = createRef<GridImperativeAPI>();
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth={25}
gridRef={gridRef}
overscanCount={0}
rowCount={100}
rowHeight={20}
/>
);
expect(gridRef.current?.element).toEqual(screen.queryByRole("grid"));
});
test("should scroll to cell", () => {
const gridRef = createRef<GridImperativeAPI>();
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={25}
columnWidth={25}
gridRef={gridRef}
overscanCount={0}
rowCount={25}
rowHeight={20}
/>
);
expect(HTMLElement.prototype.scrollTo).not.toHaveBeenCalled();
gridRef.current?.scrollToCell({ columnIndex: 4, rowIndex: 8 });
expect(HTMLElement.prototype.scrollTo).toHaveBeenCalledTimes(1);
expect(HTMLElement.prototype.scrollTo).toHaveBeenLastCalledWith({
behavior: "auto",
left: 25,
top: 140
});
});
test("should scroll to column", () => {
const gridRef = createRef<GridImperativeAPI>();
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={25}
columnWidth={25}
gridRef={gridRef}
overscanCount={0}
rowCount={25}
rowHeight={20}
/>
);
expect(HTMLElement.prototype.scrollTo).not.toHaveBeenCalled();
gridRef.current?.scrollToColumn({ index: 4 });
expect(HTMLElement.prototype.scrollTo).toHaveBeenCalledTimes(1);
expect(HTMLElement.prototype.scrollTo).toHaveBeenLastCalledWith({
behavior: "auto",
left: 25
});
});
test("should scroll to row", () => {
const gridRef = createRef<GridImperativeAPI>();
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={25}
columnWidth={25}
gridRef={gridRef}
overscanCount={0}
rowCount={25}
rowHeight={20}
/>
);
expect(HTMLElement.prototype.scrollTo).not.toHaveBeenCalled();
gridRef.current?.scrollToRow({ index: 8 });
expect(HTMLElement.prototype.scrollTo).toHaveBeenCalledTimes(1);
expect(HTMLElement.prototype.scrollTo).toHaveBeenLastCalledWith({
behavior: "auto",
top: 140
});
});
test("should throw a meaningful error if an invalid index is passed to scrollToRow", () => {
const gridRef = createRef<GridImperativeAPI>();
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={25}
columnWidth={25}
gridRef={gridRef}
overscanCount={0}
rowCount={25}
rowHeight={20}
/>
);
expect(() => {
gridRef.current?.scrollToRow({ index: -1 });
}).toThrowError("Invalid index specified: -1");
expect(() => {
gridRef.current?.scrollToRow({ index: 25 });
}).toThrowError("Invalid index specified: 25");
expect(() => {
gridRef.current?.scrollToColumn({ index: -1 });
}).toThrowError("Invalid index specified: -1");
expect(() => {
gridRef.current?.scrollToColumn({ index: 25 });
}).toThrowError("Invalid index specified: 25");
expect(() => {
gridRef.current?.scrollToCell({ columnIndex: -1, rowIndex: 0 });
}).toThrowError("Invalid index specified: -1");
expect(() => {
gridRef.current?.scrollToCell({ columnIndex: 25, rowIndex: 0 });
}).toThrowError("Invalid index specified: 25");
expect(() => {
gridRef.current?.scrollToCell({ columnIndex: 0, rowIndex: -1 });
}).toThrowError("Invalid index specified: -1");
expect(() => {
gridRef.current?.scrollToCell({ columnIndex: 0, rowIndex: 25 });
}).toThrowError("Invalid index specified: 25");
expect(HTMLElement.prototype.scrollTo).not.toHaveBeenCalled();
});
});
test("should auto-memoize cellProps object using shallow equality", () => {
const { rerender } = render(
<Grid
cellComponent={CellComponent}
cellProps={{
foo: "abc",
abc: 123
}}
columnCount={100}
columnWidth={25}
overscanCount={0}
rowCount={100}
rowHeight={20}
/>
);
expect(mountedCells).toHaveLength(8);
expect(mountedCells.get("0,0")).toMatchObject({
foo: "abc",
abc: 123
});
expect(CellComponent).toHaveBeenCalledTimes(8);
rerender(
<Grid
cellComponent={CellComponent}
cellProps={{
foo: "abc",
abc: 123
}}
columnCount={100}
columnWidth={25}
overscanCount={0}
rowCount={100}
rowHeight={20}
/>
);
expect(CellComponent).toHaveBeenCalledTimes(8);
rerender(
<Grid
cellComponent={CellComponent}
cellProps={{
foo: "abc",
abc: 234
}}
columnCount={100}
columnWidth={25}
overscanCount={0}
rowCount={100}
rowHeight={20}
/>
);
expect(CellComponent).toHaveBeenCalledTimes(16);
});
describe("edge cases", () => {
test("should not cause a cycle of Grid callback ref is passed in cellProps", () => {
function CellComponentWithCellProps({
columnIndex,
rowIndex,
style
}: CellComponentProps<{ gridRef: GridImperativeAPI | null }>) {
return (
<div style={style}>
{rowIndex},{columnIndex}
</div>
);
}
function Test() {
const [gridRef, setGridRef] = useGridCallbackRef(null);
return (
<Grid
cellComponent={CellComponentWithCellProps}
cellProps={{ gridRef }}
columnCount={100}
columnWidth={25}
gridRef={setGridRef}
overscanCount={2}
rowCount={100}
rowHeight={20}
/>
);
}
render(<Test />);
});
test("should not require ResizeObserver if size is provided", () => {
simulateUnsupportedEnvironmentForTest();
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={100}
columnWidth={25}
overscanCount={2}
rowCount={100}
rowHeight={20}
style={{ height: 42, width: 84 }}
/>
);
});
});
describe("aria attributes", () => {
test("should adhere to the best recommended practices", () => {
render(
<Grid
cellComponent={CellComponent}
cellProps={EMPTY_OBJECT}
columnCount={2}
columnWidth={25}
overscanCount={0}
rowCount={2}
rowHeight={20}
/>
);
expect(screen.queryAllByRole("grid")).toHaveLength(1);
const rows = screen.queryAllByRole("row");
expect(rows).toHaveLength(2);
expect(rows[0].getAttribute("aria-rowindex")).toBe("1");
expect(rows[1].getAttribute("aria-rowindex")).toBe("2");
expect(screen.queryAllByRole("gridcell")).toHaveLength(4);
{
const cells = rows[0].querySelectorAll('[role="gridcell"]');
expect(cells).toHaveLength(2);
expect(cells[0].getAttribute("aria-colindex")).toBe("1");
expect(cells[1].getAttribute("aria-colindex")).toBe("2");
}
{
const cells = rows[1].querySelectorAll('[role="gridcell"]');
expect(cells).toHaveLength(2);
expect(cells[0].getAttribute("aria-colindex")).toBe("1");
expect(cells[1].getAttribute("aria-colindex")).toBe("2");
}
});
});
});
================================================
FILE: lib/components/grid/Grid.tsx
================================================
"use client";
import {
createElement,
memo,
useEffect,
useImperativeHandle,
useMemo,
useState,
type ReactElement,
type ReactNode
} from "react";
import { useIsRtl } from "../../core/useIsRtl";
import { useVirtualizer } from "../../core/useVirtualizer";
import { useMemoizedObject } from "../../hooks/useMemoizedObject";
import type { Align, TagNames } from "../../types";
import { arePropsEqual } from "../../utils/arePropsEqual";
import type { GridProps } from "./types";
/**
* Renders data with many rows and columns.
*
* ℹ️ Unlike `List` rows, `Grid` cell sizes must be known ahead of time.
* Either static sizes or something that can be derived (from the data in `CellProps`) without rendering.
*/
export function Grid<
CellProps extends object,
TagName extends TagNames = "div"
>({
cellComponent: CellComponentProp,
cellProps: cellPropsUnstable,
children,
className,
columnCount,
columnWidth,
defaultHeight = 0,
defaultWidth = 0,
dir,
gridRef,
onCellsRendered,
onResize,
overscanCount = 3,
rowCount,
rowHeight,
style,
tagName = "div" as TagName,
...rest
}: GridProps<CellProps, TagName>): ReactElement {
const cellProps = useMemoizedObject(cellPropsUnstable);
const CellComponent = useMemo(
() => memo(CellComponentProp, arePropsEqual),
[CellComponentProp]
);
const [element, setElement] = useState<HTMLDivElement | null>(null);
const isRtl = useIsRtl(element, dir);
const {
getCellBounds: getColumnBounds,
getEstimatedSize: getEstimatedWidth,
startIndexOverscan: columnStartIndexOverscan,
startIndexVisible: columnStartIndexVisible,
scrollToIndex: scrollToColumnIndex,
stopIndexOverscan: columnStopIndexOverscan,
stopIndexVisible: columnStopIndexVisible
} = useVirtualizer({
containerElement: element,
containerStyle: style,
defaultContainerSize: defaultWidth,
direction: "horizontal",
isRtl,
itemCount: columnCount,
itemProps: cellProps,
itemSize: columnWidth,
onResize,
overscanCount
});
const {
getCellBounds: getRowBounds,
getEstimatedSize: getEstimatedHeight,
startIndexOverscan: rowStartIndexOverscan,
startIndexVisible: rowStartIndexVisible,
scrollToIndex: scrollToRowIndex,
stopIndexOverscan: rowStopIndexOverscan,
stopIndexVisible: rowStopIndexVisible
} = useVirtualizer({
containerElement: element,
containerStyle: style,
defaultContainerSize: defaultHeight,
direction: "vertical",
itemCount: rowCount,
itemProps: cellProps,
itemSize: rowHeight,
onResize,
overscanCount
});
useImperativeHandle(
gridRef,
() => ({
get element() {
return element;
},
scrollToCell({
behavior = "auto",
columnAlign = "auto",
columnIndex,
rowAlign = "auto",
rowIndex
}: {
behavior?: ScrollBehavior;
columnAlign?: Align;
columnIndex: number;
rowAlign?: Align;
rowIndex: number;
}) {
const left = scrollToColumnIndex({
align: columnAlign,
containerScrollOffset: element?.scrollLeft ?? 0,
index: columnIndex
});
const top = scrollToRowIndex({
align: rowAlign,
containerScrollOffset: element?.scrollTop ?? 0,
index: rowIndex
});
if (typeof element?.scrollTo === "function") {
element.scrollTo({
behavior,
left,
top
});
}
},
scrollToColumn({
align = "auto",
behavior = "auto",
index
}: {
align?: Align;
behavior?: ScrollBehavior;
index: number;
}) {
const left = scrollToColumnIndex({
align,
containerScrollOffset: element?.scrollLeft ?? 0,
index
});
if (typeof element?.scrollTo === "function") {
element.scrollTo({
behavior,
left
});
}
},
scrollToRow({
align = "auto",
behavior = "auto",
index
}: {
align?: Align;
behavior?: ScrollBehavior;
index: number;
}) {
const top = scrollToRowIndex({
align,
containerScrollOffset: element?.scrollTop ?? 0,
index
});
if (typeof element?.scrollTo === "function") {
element.scrollTo({
behavior,
top
});
}
}
}),
[element, scrollToColumnIndex, scrollToRowIndex]
);
useEffect(() => {
if (
columnStartIndexOverscan >= 0 &&
columnStopIndexOverscan >= 0 &&
rowStartIndexOverscan >= 0 &&
rowStopIndexOverscan >= 0 &&
onCellsRendered
) {
onCellsRendered(
{
columnStartIndex: columnStartIndexVisible,
columnStopIndex: columnStopIndexVisible,
rowStartIndex: rowStartIndexVisible,
rowStopIndex: rowStopIndexVisible
},
{
columnStartIndex: columnStartIndexOverscan,
columnStopIndex: columnStopIndexOverscan,
rowStartIndex: rowStartIndexOverscan,
rowStopIndex: rowStopIndexOverscan
}
);
}
}, [
onCellsRendered,
columnStartIndexOverscan,
columnStartIndexVisible,
columnStopIndexOverscan,
columnStopIndexVisible,
rowStartIndexOverscan,
rowStartIndexVisible,
rowStopIndexOverscan,
rowStopIndexVisible
]);
const cells = useMemo(() => {
const children: ReactNode[] = [];
if (columnCount > 0 && rowCount > 0) {
for (
let rowIndex = rowStartIndexOverscan;
rowIndex <= rowStopIndexOverscan;
rowIndex++
) {
const rowBounds = getRowBounds(rowIndex);
const columns: ReactNode[] = [];
for (
let columnIndex = columnStartIndexOverscan;
columnIndex <= columnStopIndexOverscan;
columnIndex++
) {
const columnBounds = getColumnBounds(columnIndex);
columns.push(
<CellComponent
{...(cellProps as CellProps)}
ariaAttributes={{
"aria-colindex": columnIndex + 1,
role: "gridcell"
}}
columnIndex={columnIndex}
key={columnIndex}
rowIndex={rowIndex}
style={{
position: "absolute",
left: isRtl ? undefined : 0,
right: isRtl ? 0 : undefined,
transform: `translate(${isRtl ? -columnBounds.scrollOffset : columnBounds.scrollOffset}px, ${rowBounds.scrollOffset}px)`,
height: rowBounds.size,
width: columnBounds.size
}}
/>
);
}
children.push(
<div key={rowIndex} role="row" aria-rowindex={rowIndex + 1}>
{columns}
</div>
);
}
}
return children;
}, [
CellComponent,
cellProps,
columnCount,
columnStartIndexOverscan,
columnStopIndexOverscan,
getColumnBounds,
getRowBounds,
isRtl,
rowCount,
rowStartIndexOverscan,
rowStopIndexOverscan
]);
const sizingElement = (
<div
aria-hidden
style={{
height: getEstimatedHeight(),
width: getEstimatedWidth(),
zIndex: -1
}}
></div>
);
return createElement(
tagName,
{
"aria-colcount": columnCount,
"aria-rowcount": rowCount,
role: "grid",
...rest,
className,
dir,
ref: setElement,
style: {
position: "relative",
maxHeight: "100%",
maxWidth: "100%",
flexGrow: 1,
overflow: "auto",
...style
}
},
cells,
children,
sizingElement
);
}
================================================
FILE: lib/components/grid/types.ts
================================================
import type {
ComponentProps,
CSSProperties,
HTMLAttributes,
ReactElement,
ReactNode,
Ref
} from "react";
import type { TagNames } from "../../types";
type ForbiddenKeys = "ariaAttributes" | "columnIndex" | "rowIndex" | "style";
type ExcludeForbiddenKeys<Type> = {
[Key in keyof Type]: Key extends ForbiddenKeys ? never : Type[Key];
};
export type GridProps<
CellProps extends object,
TagName extends TagNames = "div"
> = Omit<HTMLAttributes<HTMLDivElement>, "onResize"> & {
/**
* React component responsible for rendering a cell.
*
* This component will receive an `index` and `style` prop by default.
* Additionally it will receive prop values passed to `cellProps`.
*
* ℹ️ The prop types for this component are exported as `CellComponentProps`
*/
cellComponent: (
props: {
ariaAttributes: {
"aria-colindex": number;
role: "gridcell";
};
columnIndex: number;
rowIndex: number;
style: CSSProperties;
} & CellProps
) => ReactElement | null;
/**
* Additional props to be passed to the cell-rendering component.
* Grid will automatically re-render cells when values in this object change.
*
* ⚠️ This object must not contain `ariaAttributes`, `columnIndex`, `rowIndex`, or `style` props.
*/
cellProps: ExcludeForbiddenKeys<CellProps>;
/**
* Additional content to be rendered within the grid (above cells).
* This property can be used to render things like overlays or tooltips.
*/
children?: ReactNode;
/**
* CSS class name.
*/
className?: string;
/**
* Number of columns to be rendered in the grid.
*/
columnCount: number;
/**
* Column width; the following formats are supported:
* - number of pixels (number)
* - percentage of the grid's current width (string)
* - function that returns the column width (in pixels) given an index and `cellProps`
*/
columnWidth:
| number
| string
| ((index: number, cellProps: CellProps) => number);
/**
* Default height of grid for initial render.
* This value is important for server rendering.
*/
defaultHeight?: number;
/**
* Default width of grid for initial render.
* This value is important for server rendering.
*/
defaultWidth?: number;
/**
* Indicates the directionality of grid cells.
*
* ℹ️ See HTML `dir` [global attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir) for more information.
*/
dir?: "ltr" | "rtl";
/**
* Imperative Grid API.
*
* ℹ️ The `useGridRef` and `useGridCallbackRef` hooks are exported for convenience use in TypeScript projects.
*/
gridRef?: Ref<{
/**
* Outermost HTML element for the grid if mounted and null (if not mounted.
*/
get element(): HTMLDivElement | null;
/**
* Scrolls the grid so that the specified row and column are visible.
*
* @param behavior Determines whether scrolling is instant or animates smoothly
* @param columnAlign Determines the horizontal alignment of the element within the list
* @param columnIndex Index of the column to scroll to (0-based)
* @param rowAlign Determines the vertical alignment of the element within the list
* @param rowIndex Index of the row to scroll to (0-based)
*
* @throws RangeError if an invalid row or column index is provided
*/
scrollToCell(config: {
behavior?: "auto" | "instant" | "smooth";
columnAlign?: "auto" | "center" | "end" | "smart" | "start";
columnIndex: number;
rowAlign?: "auto" | "center" | "end" | "smart" | "start";
rowIndex: number;
}): void;
/**
* Scrolls the grid so that the specified column is visible.
*
* @param align Determines the horizontal alignment of the element within the list
* @param behavior Determines whether scrolling is instant or animates smoothly
* @param index Index of the column to scroll to (0-based)
*
* @throws RangeError if an invalid column index is provided
*/
scrollToColumn(config: {
align?: "auto" | "center" | "end" | "smart" | "start";
behavior?: "auto" | "instant" | "smooth";
index: number;
}): void;
/**
* Scrolls the grid so that the specified row is visible.
*
* @param align Determines the vertical alignment of the element within the list
* @param behavior Determines whether scrolling is instant or animates smoothly
* @param index Index of the row to scroll to (0-based)
*
* @throws RangeError if an invalid row index is provided
*/
scrollToRow(config: {
align?: "auto" | "center" | "end" | "smart" | "start";
behavior?: "auto" | "instant" | "smooth";
index: number;
}): void;
}>;
/**
* Callback notified when the range of rendered cells changes.
*/
onCellsRendered?: (
visibleCells: {
columnStartIndex: number;
columnStopIndex: number;
rowStartIndex: number;
rowStopIndex: number;
},
allCells: {
columnStartIndex: number;
columnStopIndex: number;
rowStartIndex: number;
rowStopIndex: number;
}
) => void;
/**
* Callback notified when the Grid's outermost HTMLElement resizes.
* This may be used to (re)scroll a cell into view.
*/
onResize?: (
size: { height: number; width: number },
prevSize: { height: number; width: number }
) => void;
/**
* How many additional rows/columns to render outside of the visible area.
* This can reduce visual flickering near the edges of a grid when scrolling.
*/
overscanCount?: number;
/**
* Number of rows to be rendered in the grid.
*/
rowCount: number;
/**
* Row height; the following formats are supported:
* - number of pixels (number)
* - percentage of the grid's current height (string)
* - function that returns the row height (in pixels) given an index and `cellProps`
*/
rowHeight:
| number
| string
| ((index: number, cellProps: CellProps) => number);
/**
* Optional CSS properties.
* The grid of cells will fill the height and width defined by this style.
*/
style?: CSSProperties;
/**
* Can be used to override the root HTML element rendered by the List component.
* The default value is "div", meaning that List renders an HTMLDivElement as its root.
*
* ⚠️ In most use cases the default ARIA roles are sufficient and this prop is not needed.
*/
tagName?: TagName;
};
export type CellComponent<CellProps extends object> =
GridProps<CellProps>["cellComponent"];
export type CellComponentProps<CellProps extends object = object> =
ComponentProps<CellComponent<CellProps>>;
export type ScrollState = {
prevScrollTop: number;
scrollTop: number;
};
export type OnCellsRendered = NonNullable<GridProps<object>["onCellsRendered"]>;
export type CachedBounds = Map<
number,
{
height: number;
scrollTop: number;
}
>;
/**
* Ref used to interact with this component's imperative API.
*
* This API has imperative methods for scrolling and a getter for the outermost DOM element.
*
* ℹ️ The `useGridRef` and `useGridCallbackRef` hooks are exported for convenience use in TypeScript projects.
*/
export interface GridImperativeAPI {
/**
* Outermost HTML element for the grid if mounted and null (if not mounted.
*/
get element(): HTMLDivElement | null;
/**
* Scrolls the grid so that the specified row and column are visible.
*
* @param behavior Determines whether scrolling is instant or animates smoothly
* @param columnAlign Determines the horizontal alignment of the element within the list
* @param columnIndex Index of the column to scroll to (0-based)
* @param rowAlign Determines the vertical alignment of the element within the list
* @param rowIndex Index of the row to scroll to (0-based)
*
* @throws RangeError if an invalid row or column index is provided
*/
scrollToCell: ({
behavior,
columnAlign,
columnIndex,
rowAlign,
rowIndex
}: {
behavior?: "auto" | "instant" | "smooth";
columnAlign?: "auto" | "center" | "end" | "smart" | "start";
columnIndex: number;
rowAlign?: "auto" | "center" | "end" | "smart" | "start";
rowIndex: number;
}) => void;
/**
* Scrolls the grid so that the specified column is visible.
*
* @param align Determines the horizontal alignment of the element within the list
* @param behavior Determines whether scrolling is instant or animates smoothly
* @param index Index of the column to scroll to (0-based)
*
* @throws RangeError if an invalid column index is provided
*/
scrollToColumn: ({
align,
behavior,
index
}: {
align?: "auto" | "center" | "end" | "smart" | "start";
behavior?: "auto" | "instant" | "smooth";
index: number;
}) => void;
/**
* Scrolls the grid so that the specified row is visible.
*
* @param align Determines the vertical alignment of the element within the list
* @param behavior Determines whether scrolling is instant or animates smoothly
* @param index Index of the row to scroll to (0-based)
*
* @throws RangeError if an invalid row index is provided
*/
scrollToRow: ({
align,
behavior,
index
}: {
align?: "auto" | "center" | "end" | "smart" | "start";
behavior?: "auto" | "instant" | "smooth";
index: number;
}) => void;
}
================================================
FILE: lib/components/grid/useGridCallbackRef.ts
================================================
import { useState } from "react";
import type { GridImperativeAPI } from "./types";
/**
* Convenience hook to return a properly typed ref callback for the Grid component.
*
* Use this hook when you need to share the ref with another component or hook.
*/
export const useGridCallbackRef =
useState as typeof useState<GridImperativeAPI | null>;
================================================
FILE: lib/components/grid/useGridRef.ts
================================================
import { useRef } from "react";
import type { GridImperativeAPI } from "./types";
/**
* Convenience hook to return a properly typed ref for the Grid component.
*/
export const useGridRef = useRef as typeof useRef<GridImperativeAPI>;
================================================
FILE: lib/components/list/List.test.tsx
================================================
import { act, render, screen } from "@testing-library/react";
import { createRef, useLayoutEffect } from "react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { EMPTY_OBJECT } from "../../../src/constants";
import { assert } from "../../utils/assert";
import {
disableResizeObserverForCurrentTest,
setDefaultElementSize,
setElementSize,
setElementSizeFunction,
simulateUnsupportedEnvironmentForTest
} from "../../utils/test/mockResizeObserver";
import { DATA_ATTRIBUTE_LIST_INDEX, List } from "./List";
import { type ListImperativeAPI, type RowComponentProps } from "./types";
import { useDynamicRowHeight } from "./useDynamicRowHeight";
import { useListCallbackRef } from "./useListCallbackRef";
describe("List", () => {
let mountedRows: Map<number, RowComponentProps<object>> = new Map();
const RowComponent = vi.fn(function Row(props: RowComponentProps<object>) {
const { ariaAttributes, index, style } = props;
useLayoutEffect(() => {
mountedRows.set(index, props);
return () => {
mountedRows.delete(index);
};
});
return (
<div {...ariaAttributes} style={style}>
Row {index}
</div>
);
});
beforeEach(() => {
RowComponent.mockReset();
setDefaultElementSize({ height: 100, width: 50 });
mountedRows = new Map();
});
test("should render an empty list", () => {
render(
<List
rowCount={0}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
const items = screen.queryAllByRole("listitem");
expect(items).toHaveLength(0);
});
test("should render enough rows to fill the available height", () => {
const onResize = vi.fn();
const { container } = render(
<List
onResize={onResize}
overscanCount={0}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
let items = screen.queryAllByRole("listitem");
expect(items).toHaveLength(4);
expect(items[0]).toHaveTextContent("Row 0");
expect(items[3]).toHaveTextContent("Row 3");
expect(onResize).toBeCalledTimes(1);
expect(onResize).toHaveBeenLastCalledWith(
{
height: 100,
width: 50
},
{
height: 0,
width: 0
}
);
act(() => {
const listElement = container.querySelector<HTMLElement>('[role="list"]');
assert(listElement !== null);
setElementSize({
element: listElement,
height: 75,
width: 50
});
});
items = screen.queryAllByRole("listitem");
expect(items).toHaveLength(3);
expect(items[0]).toHaveTextContent("Row 0");
expect(items[2]).toHaveTextContent("Row 2");
expect(onResize).toBeCalledTimes(2);
expect(onResize).toHaveBeenLastCalledWith(
{
height: 75,
width: 50
},
{
height: 100,
width: 50
}
);
});
test("should render enough rows to fill the available height with overscan", () => {
const { container } = render(
<List
overscanCount={2}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
let items = screen.queryAllByRole("listitem");
expect(items).toHaveLength(6);
expect(items[0]).toHaveTextContent("Row 0");
expect(items[5]).toHaveTextContent("Row 5");
act(() => {
const listElement = container.querySelector<HTMLElement>('[role="list"]');
assert(listElement !== null);
setElementSize({
element: listElement,
height: 75,
width: 50
});
});
items = screen.queryAllByRole("listitem");
expect(items).toHaveLength(5);
expect(items[0]).toHaveTextContent("Row 0");
expect(items[4]).toHaveTextContent("Row 4");
});
test("should pass rowProps to the rowComponent", () => {
render(
<List
overscanCount={1}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={{
foo: "abc",
bar: 123
}}
/>
);
expect(mountedRows.size).toEqual(5);
expect(mountedRows.get(0)).toMatchObject({
foo: "abc",
bar: 123
});
});
test("should re-render items if rowComponent changes", () => {
const { rerender } = render(
<List
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
const NewRowComponent = vi.fn(() => <div />);
rerender(
<List
rowCount={100}
rowComponent={NewRowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
expect(NewRowComponent).toHaveBeenCalled();
});
test("should re-render items if rowHeight changes", () => {
const { rerender } = render(
<List
overscanCount={1}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
expect(mountedRows).toHaveLength(5);
rerender(
<List
overscanCount={1}
rowCount={100}
rowComponent={RowComponent}
rowHeight={50}
rowProps={EMPTY_OBJECT}
/>
);
expect(mountedRows).toHaveLength(3);
expect(mountedRows.get(1)?.index).toEqual(1);
});
test("should re-render items if rowProps change", () => {
const { rerender } = render(
<List
overscanCount={1}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={{
foo: "abc"
}}
/>
);
expect(mountedRows).toHaveLength(5);
expect(mountedRows.get(0)).toMatchObject({
foo: "abc"
});
rerender(
<List
overscanCount={1}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={{
bar: 123
}}
/>
);
expect(mountedRows).toHaveLength(5);
expect(mountedRows.get(1)?.index).toEqual(1);
expect(mountedRows.get(0)).toMatchObject({
bar: 123
});
});
test("should use defaultHeight for initial mount", () => {
// Mimic server rendering
disableResizeObserverForCurrentTest();
render(
<List
overscanCount={0}
defaultHeight={75}
rowCount={4}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
const items = screen.queryAllByRole("listitem");
expect(items).toHaveLength(3);
});
test("should call onRowsRendered", () => {
const onRowsRendered = vi.fn();
const { rerender } = render(
<List
overscanCount={0}
defaultHeight={100}
rowCount={2}
onRowsRendered={onRowsRendered}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
expect(onRowsRendered).toHaveBeenCalledTimes(1);
expect(onRowsRendered).toHaveBeenLastCalledWith(
{
startIndex: 0,
stopIndex: 1
},
{
startIndex: 0,
stopIndex: 1
}
);
rerender(
<List
overscanCount={2}
rowCount={4}
onRowsRendered={onRowsRendered}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
expect(onRowsRendered).toHaveBeenCalledTimes(2);
expect(onRowsRendered).toHaveBeenLastCalledWith(
{
startIndex: 0,
stopIndex: 3
},
{
startIndex: 0,
stopIndex: 3
}
);
rerender(
<List
overscanCount={2}
rowCount={10}
onRowsRendered={onRowsRendered}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
expect(onRowsRendered).toHaveBeenCalledTimes(3);
expect(onRowsRendered).toHaveBeenLastCalledWith(
{
startIndex: 0,
stopIndex: 3
},
{
startIndex: 0,
stopIndex: 5
}
);
});
test("should support custom className and style props", () => {
render(
<List
overscanCount={0}
className="foo"
role="list"
rowCount={4}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
style={{
backgroundColor: "red"
}}
/>
);
const list = screen.queryByRole("list");
expect(list).toHaveClass("foo");
expect(list?.style.backgroundColor).toBe("red");
});
test("should spread HTML rest attributes", () => {
render(
<List
overscanCount={0}
data-testid="foo"
role="list"
rowCount={4}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
expect(screen.queryByTestId("foo")).toHaveRole("list");
});
test("custom tagName and attributes", () => {
function CustomRowComponent({ index, style }: RowComponentProps<object>) {
return <li style={style}>Row {index + 1}</li>;
}
const { container } = render(
<List
overscanCount={0}
rowCount={4}
rowComponent={CustomRowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
tagName="ul"
/>
);
expect(container.firstElementChild?.tagName).toBe("UL");
expect(container.querySelectorAll("LI")).toHaveLength(4);
});
test("children", () => {
const { container } = render(
<List
overscanCount={0}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
>
<div id="custom">Overlay or tooltip</div>
</List>
);
expect(container.querySelector("#custom")).toHaveTextContent(
"Overlay or tooltip"
);
});
describe("imperative API", () => {
test("should return the root element", () => {
const listRef = createRef<ListImperativeAPI>();
render(
<List
listRef={listRef}
role="list"
rowComponent={RowComponent}
rowCount={4}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
expect(listRef.current?.element).toEqual(screen.queryByRole("list"));
});
test("should scroll to rows", () => {
const listRef = createRef<ListImperativeAPI>();
render(
<List
rowCount={25}
listRef={listRef}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
expect(HTMLElement.prototype.scrollTo).not.toHaveBeenCalled();
listRef.current?.scrollToRow({ index: 8 });
expect(HTMLElement.prototype.scrollTo).toHaveBeenCalledTimes(1);
expect(HTMLElement.prototype.scrollTo).toHaveBeenLastCalledWith({
behavior: "auto",
top: 125
});
});
test("should throw a meaningful error if an invalid index is passed to scrollToRow", () => {
const listRef = createRef<ListImperativeAPI>();
render(
<List
rowCount={25}
listRef={listRef}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
expect(() => {
listRef.current?.scrollToRow({ index: -1 });
}).toThrowError("Invalid index specified: -1");
expect(() => {
listRef.current?.scrollToRow({ index: 25 });
}).toThrowError("Invalid index specified: 25");
expect(HTMLElement.prototype.scrollTo).not.toHaveBeenCalled();
});
});
test("should auto-memoize rowProps object using shallow equality", () => {
const { rerender } = render(
<List
overscanCount={1}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={{
foo: "abc",
abc: 123
}}
/>
);
expect(mountedRows).toHaveLength(5);
expect(mountedRows.get(0)).toMatchObject({
foo: "abc",
abc: 123
});
expect(RowComponent).toHaveBeenCalledTimes(5);
rerender(
<List
overscanCount={1}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={{
foo: "abc",
abc: 123
}}
/>
);
expect(RowComponent).toHaveBeenCalledTimes(5);
rerender(
<List
overscanCount={1}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={{
foo: "abc",
abc: 234
}}
/>
);
expect(RowComponent).toHaveBeenCalledTimes(10);
});
describe("rowHeight", () => {
test("type: number (px)", () => {
const { container } = render(
<List
overscanCount={0}
rowCount={50}
rowComponent={RowComponent}
rowHeight={50}
rowProps={EMPTY_OBJECT}
/>
);
expect(container.querySelectorAll('[role="listitem"]')).toHaveLength(2);
});
test("type: function (px)", () => {
const rowHeight = (index: number) => 25 + index * 25;
const { container } = render(
<List
overscanCount={0}
rowCount={50}
rowComponent={RowComponent}
rowHeight={rowHeight}
rowProps={EMPTY_OBJECT}
/>
);
expect(container.querySelectorAll('[role="listitem"]')).toHaveLength(3);
});
test("type: string (%)", () => {
const { container } = render(
<List
overscanCount={0}
rowCount={50}
rowComponent={RowComponent}
rowHeight="25%"
rowProps={EMPTY_OBJECT}
/>
);
expect(container.querySelectorAll('[role="listitem"]')).toHaveLength(4);
});
describe("type: DynamicRowHeight", () => {
let onRowsRendered: ReturnType<typeof vi.fn>;
function Example() {
const rowHeight = useDynamicRowHeight({
defaultRowHeight: 25
});
return (
<List
defaultHeight={100}
overscanCount={0}
onRowsRendered={onRowsRendered}
rowCount={10}
rowComponent={RowComponent}
rowHeight={rowHeight}
rowProps={EMPTY_OBJECT}
/>
);
}
function setMockRowHeights(
indexToHeight: Map<number, number>,
defaultHeight: number = 25
) {
setElementSizeFunction((element) => {
const attribute = element.getAttribute(DATA_ATTRIBUTE_LIST_INDEX);
if (attribute !== null) {
const index = parseInt(attribute);
const height = indexToHeight.get(index) ?? defaultHeight;
return new DOMRect(0, 0, 100, height);
}
});
}
beforeEach(() => {
onRowsRendered = vi.fn();
});
test("initial measuring", () => {
setMockRowHeights(
new Map([
[0, 20],
[1, 40],
[2, 60],
[3, 80]
])
);
const { container } = render(<Example />);
// 4 rows based on initial estimate
// 3 rows after actual sizes have been measured
expect(onRowsRendered).toHaveBeenCalledTimes(2);
expect(onRowsRendered).nthCalledWith(
1,
{
startIndex: 0,
stopIndex: 3
},
{
startIndex: 0,
stopIndex: 3
}
);
expect(onRowsRendered).nthCalledWith(
2,
{
startIndex: 0,
stopIndex: 2
},
{
startIndex: 0,
stopIndex: 2
}
);
expect(
container.querySelector<HTMLDivElement>("[aria-hidden]")?.style.height
).toBe("500px");
});
test("caching and invalidation", () => {
setMockRowHeights(
new Map([
[0, 20],
[1, 40],
[2, 60],
[3, 80]
])
);
render(<Example />);
expect(RowComponent).toHaveBeenCalledTimes(4 + 3);
RowComponent.mockReset();
act(() => {
setMockRowHeights(
new Map([
[0, 20],
[1, 50], // Changed
[2, 60],
[3, 80]
])
);
});
// Only the row that has been nudged down should be re-rendered;
// the other two should be memoized
expect(RowComponent).toHaveBeenCalledTimes(1);
expect(RowComponent).toHaveBeenLastCalledWith(
expect.objectContaining({
index: 2
}),
undefined
);
});
});
});
describe("edge cases", () => {
test("should restore scroll indices if rowProps changes", () => {
const listRef = createRef<ListImperativeAPI>();
const onRowsRendered = vi.fn();
const { rerender } = render(
<List
listRef={listRef}
onRowsRendered={onRowsRendered}
overscanCount={0}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={{
foo: 1
}}
/>
);
expect(onRowsRendered).toHaveBeenCalled();
expect(onRowsRendered).toHaveBeenLastCalledWith(
{
startIndex: 0,
stopIndex: 3
},
{
startIndex: 0,
stopIndex: 3
}
);
onRowsRendered.mockReset();
act(() => {
listRef.current?.scrollToRow({ index: 10 });
});
expect(onRowsRendered).toHaveBeenCalledTimes(1);
expect(onRowsRendered).toHaveBeenLastCalledWith(
{
startIndex: 7,
stopIndex: 10
},
{
startIndex: 7,
stopIndex: 10
}
);
expect(RowComponent).toHaveBeenLastCalledWith(
expect.objectContaining({
foo: 1
}),
undefined
);
onRowsRendered.mockReset();
RowComponent.mockReset();
rerender(
<List
listRef={listRef}
onRowsRendered={onRowsRendered}
overscanCount={0}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={{
foo: 2
}}
/>
);
// Visible range of rows should not have changes
expect(onRowsRendered).not.toHaveBeenCalled();
// But rows should have been re-rendered
expect(RowComponent).toHaveBeenCalledTimes(4);
expect(RowComponent).toHaveBeenLastCalledWith(
expect.objectContaining({
foo: 2
}),
undefined
);
});
test("should handle temporarily invalid indices if rowCount decreases", () => {
function CustomRowComponent({
index,
items,
style
}: RowComponentProps<{ items: string[] }>) {
return <div style={style}>{items[index].toUpperCase()}</div>;
}
const { container, rerender } = render(
<List
overscanCount={0}
rowCount={5}
rowComponent={CustomRowComponent}
rowHeight={25}
rowProps={{
items: ["a", "b", "c", "d", "e"]
}}
/>
);
expect(container.textContent).toEqual("ABCD");
rerender(
<List
overscanCount={0}
rowCount={2}
rowComponent={CustomRowComponent}
rowHeight={25}
rowProps={{
items: ["a", "b"]
}}
/>
);
expect(container.textContent).toEqual("AB");
});
test("should not cause a cycle of List callback ref is passed in rowProps", () => {
function RowComponentWithRowProps({
index,
style
}: RowComponentProps<{ listRef: ListImperativeAPI | null }>) {
return <div style={style}>{index}</div>;
}
function Test() {
const [listRef, setListRef] = useListCallbackRef(null);
return (
<List
listRef={setListRef}
rowComponent={RowComponentWithRowProps}
rowCount={10}
rowHeight={25}
rowProps={{ listRef }}
/>
);
}
render(<Test />);
});
test("should not require ResizeObserver if height is provided", () => {
simulateUnsupportedEnvironmentForTest();
render(
<List
overscanCount={0}
rowCount={100}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
style={{ height: 42 }}
/>
);
});
});
describe("aria attributes", () => {
test("should be set by default", () => {
render(
<List
rowCount={3}
rowComponent={RowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
expect(screen.queryAllByRole("list")).toHaveLength(1);
const rows = screen.queryAllByRole("listitem");
expect(rows).toHaveLength(3);
expect(rows[0].getAttribute("aria-posinset")).toBe("1");
expect(rows[0].getAttribute("aria-setsize")).toBe("3");
expect(rows[1].getAttribute("aria-posinset")).toBe("2");
expect(rows[1].getAttribute("aria-setsize")).toBe("3");
expect(rows[2].getAttribute("aria-posinset")).toBe("3");
expect(rows[2].getAttribute("aria-setsize")).toBe("3");
});
test("should support overrides for use cases like tabular data", () => {
const TableRowComponent = (props: RowComponentProps<object>) => {
const { index, style } = props;
return (
<div aria-rowindex={index + 1} role="row" style={style}>
<div role="cell" aria-colindex={1} />
<div role="cell" aria-colindex={2} />
<div role="cell" aria-colindex={3} />
</div>
);
};
render(
<List
role="table"
aria-colcount={3}
aria-rowcount={2}
rowCount={2}
rowComponent={TableRowComponent}
rowHeight={25}
rowProps={EMPTY_OBJECT}
/>
);
const tables = screen.queryAllByRole("table");
expect(tables).toHaveLength(1);
expect(tables[0].getAttribute("aria-colcount")).toBe("3");
expect(tables[0].getAttribute("aria-rowcount")).toBe("2");
const rows = screen.queryAllByRole("row");
expect(rows).toHaveLength(2);
const columns = rows[0].querySelectorAll('[role="cell"]');
expect(columns).toHaveLength(3);
expect(columns[0].getAttribute("aria-colindex")).toBe("1");
expect(columns[1].getAttribute("aria-colindex")).toBe("2");
expect(columns[2].getAttribute("aria-colindex")).toBe("3");
});
});
});
================================================
FILE: lib/components/list/List.tsx
================================================
"use client";
import {
createElement,
memo,
useEffect,
useImperativeHandle,
useMemo,
useState,
type ReactElement,
type ReactNode
} from "react";
import { useVirtualizer } from "../../core/useVirtualizer";
import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect";
import { useMemoizedObject } from "../../hooks/useMemoizedObject";
import type { Align, TagNames } from "../../types";
import { arePropsEqual } from "../../utils/arePropsEqual";
import { isDynamicRowHeight as isDynamicRowHeightUtil } from "./isDynamicRowHeight";
import type { ListProps } from "./types";
export const DATA_ATTRIBUTE_LIST_INDEX = "data-react-window-index";
/**
* Renders data with many rows.
*/
export function List<
RowProps extends object,
TagName extends TagNames = "div"
>({
children,
className,
defaultHeight = 0,
listRef,
onResize,
onRowsRendered,
overscanCount = 3,
rowComponent: RowComponentProp,
rowCount,
rowHeight: rowHeightProp,
rowProps: rowPropsUnstable,
tagName = "div" as TagName,
style,
...rest
}: ListProps<RowProps, TagName>): ReactElement {
const rowProps = useMemoizedObject(rowPropsUnstable);
const RowComponent = useMemo(
() => memo(RowComponentProp, arePropsEqual),
[RowComponentProp]
);
const [element, setElement] = useState<HTMLDivElement | null>(null);
const isDynamicRowHeight = isDynamicRowHeightUtil(rowHeightProp);
const rowHeight = useMemo(() => {
if (isDynamicRowHeight) {
return (index: number) => {
return (
rowHeightProp.getRowHeight(index) ??
rowHeightProp.getAverageRowHeight()
);
};
}
return rowHeightProp;
}, [isDynamicRowHeight, rowHeightProp]);
const {
getCellBounds,
getEstimatedSize,
scrollToIndex,
startIndexOverscan,
startIndexVisible,
stopIndexOverscan,
stopIndexVisible
} = useVirtualizer({
containerElement: element,
containerStyle: style,
defaultContainerSize: defaultHeight,
direction: "vertical",
itemCount: rowCount,
itemProps: rowProps,
itemSize: rowHeight,
onResize,
overscanCount
});
useImperativeHandle(
listRef,
() => ({
get element() {
return element;
},
scrollToRow({
align = "auto",
behavior = "auto",
index
}: {
align?: Align;
behavior?: ScrollBehavior;
index: number;
}) {
const top = scrollToIndex({
align,
containerScrollOffset: element?.scrollTop ?? 0,
index
});
if (typeof element?.scrollTo === "function") {
element.scrollTo({
behavior,
top
});
}
}
}),
[element, scrollToIndex]
);
useIsomorphicLayoutEffect(() => {
if (!element) {
return;
}
const rows = Array.from(element.children).filter((item, index) => {
if (item.hasAttribute("aria-hidden")) {
// Ignore sizing element
return false;
}
const attribute = `${startIndexOverscan + index}`;
item.setAttribute(DATA_ATTRIBUTE_LIST_INDEX, attribute);
return true;
});
if (isDynamicRowHeight) {
return rowHeightProp.observeRowElements(rows);
}
}, [
element,
isDynamicRowHeight,
rowHeightProp,
startIndexOverscan,
stopIndexOverscan
]);
useEffect(() => {
if (startIndexOverscan >= 0 && stopIndexOverscan >= 0 && onRowsRendered) {
onRowsRendered(
{
startIndex: startIndexVisible,
stopIndex: stopIndexVisible
},
{
startIndex: startIndexOverscan,
stopIndex: stopIndexOverscan
}
);
}
}, [
onRowsRendered,
startIndexOverscan,
startIndexVisible,
stopIndexOverscan,
stopIndexVisible
]);
const rows = useMemo(() => {
const children: ReactNode[] = [];
if (rowCount > 0) {
for (
let index = startIndexOverscan;
index <= stopIndexOverscan;
index++
) {
const bounds = getCellBounds(index);
children.push(
<RowComponent
{...(rowProps as RowProps)}
ariaAttributes={{
"aria-posinset": index + 1,
"aria-setsize": rowCount,
role: "listitem"
}}
key={index}
index={index}
style={{
position: "absolute",
left: 0,
transform: `translateY(${bounds.scrollOffset}px)`,
// In case of dynamic row heights, don't specify a height style
// otherwise a default/estimated height would mask the actual height
height: isDynamicRowHeight ? undefined : bounds.size,
width: "100%"
}}
/>
);
}
}
return children;
}, [
RowComponent,
getCellBounds,
isDynamicRowHeight,
rowCount,
rowProps,
startIndexOverscan,
stopIndexOverscan
]);
const sizingElement = (
<div
aria-hidden
style={{
height: getEstimatedSize(),
width: "100%",
zIndex: -1
}}
></div>
);
return createElement(
tagName,
{
role: "list",
...rest,
className,
ref: setElement,
style: {
position: "relative",
maxHeight: "100%",
flexGrow: 1,
overflowY: "auto",
...style
}
},
rows,
children,
sizingElement
);
}
================================================
FILE: lib/components/list/isDynamicRowHeight.ts
================================================
import type { DynamicRowHeight } from "./types";
export function isDynamicRowHeight(value: unknown): value is DynamicRowHeight {
return (
value != null &&
typeof value === "object" &&
"getAverageRowHeight" in value &&
typeof value.getAverageRowHeight === "function"
);
}
================================================
FILE: lib/components/list/types.ts
================================================
import type {
ComponentProps,
CSSProperties,
HTMLAttributes,
ReactElement,
ReactNode,
Ref
} from "react";
import type { TagNames } from "../../types";
export type DynamicRowHeight = {
getAverageRowHeight(): number;
getRowHeight(index: number): number | undefined;
setRowHeight(index: number, size: number): void;
observeRowElements: (elements: Element[] | NodeListOf<Element>) => () => void;
};
type ForbiddenKeys = "ariaAttributes" | "index" | "style";
type ExcludeForbiddenKeys<Type> = {
[Key in keyof Type]: Key extends ForbiddenKeys ? never : Type[Key];
};
export type ListProps<
RowProps extends object,
TagName extends TagNames = "div"
> = Omit<HTMLAttributes<HTMLDivElement>, "onResize"> & {
/**
* Additional content to be rendered within the list (above cells).
* This property can be used to render things like overlays or tooltips.
*/
children?: ReactNode;
/**
* CSS class name.
*/
className?: string;
/**
* Default height of list for initial render.
* This value is important for server rendering.
*/
defaultHeight?: number;
/**
* Ref used to interact with this component's imperative API.
*
* This API has imperative methods for scrolling and a getter for the outermost DOM element.
*
* ℹ️ The `useListRef` and `useListCallbackRef` hooks are exported for convenience use in TypeScript projects.
*/
listRef?: Ref<{
/**
* Outermost HTML element for the list if mounted and null (if not mounted.
*/
get element(): HTMLDivElement | null;
/**
* Scrolls the list so that the specified row is visible.
*
* @param align Determines the vertical alignment of the element within the list
* @param behavior Determines whether scrolling is instant or animates smoothly
* @param index Index of the row to scroll to (0-based)
*
* @throws RangeError if an invalid row index is provided
*/
scrollToRow(config: {
align?: "auto" | "center" | "end" | "smart" | "start";
behavior?: "auto" | "instant" | "smooth";
index: number;
}): void;
}>;
/**
* Callback notified when the List's outermost HTMLElement resizes.
* This may be used to (re)scroll a row into view.
*/
onResize?: (
size: { height: number; width: number },
prevSize: { height: number; width: number }
) => void;
/**
* Callback notified when the range of visible rows changes.
*/
onRowsRendered?: (
visibleRows: { startIndex: number; stopIndex: number },
allRows: { startIndex: number; stopIndex: number }
) => void;
/**
* How many additional rows to render outside of the visible area.
* This can reduce visual flickering near the edges of a list when scrolling.
*/
overscanCount?: number;
/**
* React component responsible for rendering a row.
*
* This component will receive an `index` and `style` prop by default.
* Additionally it will receive prop values passed to `rowProps`.
*
* ℹ️ The prop types for this component are exported as `RowComponentProps`
*/
rowComponent: (
props: {
ariaAttributes: {
"aria-posinset": number;
"aria-setsize": number;
role: "listitem";
};
index: number;
style: CSSProperties;
} & RowProps
) => ReactElement | null;
/**
* Number of items to be rendered in the list.
*/
rowCount: number;
/**
* Row height; the following formats are supported:
* - number of pixels (number)
* - percentage of the grid's current height (string)
* - function that returns the row height (in pixels) given an index and `cellProps`
* - dynamic row height cache returned by the `useDynamicRowHeight` hook
*
* ⚠️ Dynamic row heights are not as efficient as predetermined sizes.
* It's recommended to provide your own height values if they can be determined ahead of time.
*/
rowHeight:
| number
| string
| ((index: number, cellProps: RowProps) => number)
| DynamicRowHeight;
/**
* Additional props to be passed to the row-rendering component.
* List will automatically re-render rows when values in this object change.
*
* ⚠️ This object must not contain `ariaAttributes`, `index`, or `style` props.
*/
rowProps: ExcludeForbiddenKeys<RowProps>;
/**
* Optional CSS properties.
* The list of rows will fill the height defined by this style.
*/
style?: CSSProperties;
/**
* Can be used to override the root HTML element rendered by the List component.
* The default value is "div", meaning that List renders an HTMLDivElement as its root.
*
* ⚠️ In most use cases the default ARIA roles are sufficient and this prop is not needed.
*/
tagName?: TagName;
};
export type RowComponent<RowProps extends object> =
ListProps<RowProps>["rowComponent"];
export type RowComponentProps<RowProps extends object = object> =
ComponentProps<RowComponent<RowProps>>;
export type OnRowsRendered = NonNullable<ListProps<object>["onRowsRendered"]>;
export type CachedBounds = Map<
number,
{
height: number;
scrollTop: number;
}
>;
/**
* Imperative List API.
*
* ℹ️ The `useListRef` and `useListCallbackRef` hooks are exported for convenience use in TypeScript projects.
*/
export interface ListImperativeAPI {
/**
* Outermost HTML element for the list if mounted and null (if not mounted.
*/
get element(): HTMLDivElement | null;
/**
* Scrolls the list so that the specified row is visible.
*
* @param align Determines the vertical alignment of the element within the list
* @param behavior Determines whether scrolling is instant or animates smoothly
* @param index Index of the row to scroll to (0-based)
*
* @throws RangeError if an invalid row index is provided
*/
scrollToRow: ({
align,
behavior,
index
}: {
align?: "auto" | "center" | "end" | "smart" | "start";
behavior?: "auto" | "instant" | "smooth";
index: number;
}) => void;
}
================================================
FILE: lib/components/list/useDynamicRowHeight.test.ts
================================================
import { act, renderHook } from "@testing-library/react";
import { describe, expect, test } from "vitest";
import { useDynamicRowHeight } from "./useDynamicRowHeight";
import { DATA_ATTRIBUTE_LIST_INDEX } from "./List";
import { setElementSize } from "../../utils/test/mockResizeObserver";
import { NOOP_FUNCTION } from "../../../src/constants";
describe("useDynamicRowHeight", () => {
describe("getAverageRowHeight", () => {
test("returns an initial estimate based on the defaultRowHeight", () => {
const { result } = renderHook(() =>
useDynamicRowHeight({
defaultRowHeight: 100
})
);
expect(result.current.getAverageRowHeight()).toBe(100);
});
test("returns an estimate based on measured rows", () => {
const { result } = renderHook(() =>
useDynamicRowHeight({
defaultRowHeight: 100
})
);
act(() => {
result.current.setRowHeight(0, 10);
result.current.setRowHeight(1, 20);
});
expect(result.current.getAverageRowHeight()).toBe(15);
act(() => {
result.current.setRowHeight(2, 30);
});
expect(result.current.getAverageRowHeight()).toBe(20);
act(() => {
result.current.setRowHeight(2, 15);
});
expect(result.current.getAverageRowHeight()).toBe(15);
});
test("resets when key changes", () => {
const { result, rerender } = renderHook((key: string = "a") =>
useDynamicRowHeight({
defaultRowHeight: 100,
key
})
);
act(() => {
result.current.setRowHeight(0, 10);
});
expect(result.current.getAverageRowHeight()).toBe(10);
rerender("a");
expect(result.current.getAverageRowHeight()).toBe(10);
rerender("b");
expect(result.current.getAverageRowHeight()).toBe(100);
});
});
describe("getRowHeight", () => {
test("returns estimated height for a row that has not yet been measured", () => {
const { result } = renderHook(() =>
useDynamicRowHeight({
defaultRowHeight: 100
})
);
expect(result.current.getRowHeight(0)).toBe(100);
});
test("returns the most recently measured size", () => {
const { result } = renderHook(() =>
useDynamicRowHeight({
defaultRowHeight: 100
})
);
act(() => {
result.current.setRowHeight(0, 15);
result.current.setRowHeight(1, 20);
result.current.setRowHeight(3, 25);
});
expect(result.current.getRowHeight(0)).toBe(15);
expect(result.current.getRowHeight(1)).toBe(20);
expect(result.current.getRowHeight(2)).toBe(100);
expect(result.current.getRowHeight(3)).toBe(25);
act(() => {
result.current.setRowHeight(1, 25);
});
expect(result.current.getRowHeight(1)).toBe(25);
});
test("resets when key changes", () => {
const { result, rerender } = renderHook((key: string = "a") =>
useDynamicRowHeight({
defaultRowHeight: 100,
key
})
);
act(() => {
result.current.setRowHeight(0, 10);
});
expect(result.current.getRowHeight(0)).toBe(10);
rerender("a");
expect(result.current.getRowHeight(0)).toBe(10);
rerender("b");
expect(result.current.getRowHeight(0)).toBe(100);
});
});
describe("observeRowElements", () => {
function createRowElement(index: number) {
const element = document.createElement("div");
element.setAttribute(DATA_ATTRIBUTE_LIST_INDEX, "" + index);
return element;
}
test("should update cache when an observed element is resized", () => {
const { result } = renderHook(() =>
useDynamicRowHeight({
defaultRowHeight: 100
})
);
const elementA = createRowElement(0);
const elementB = createRowElement(1);
act(() => {
result.current.observeRowElements([elementA, elementB]);
});
expect(result.current.getRowHeight(0)).toBe(100);
expect(result.current.getRowHeight(1)).toBe(100);
act(() => {
setElementSize({
element: elementB,
width: 100,
height: 20
});
});
expect(result.current.getRowHeight(0)).toBe(100);
expect(result.current.getRowHeight(1)).toBe(20);
act(() => {
setElementSize({
element: elementA,
width: 100,
height: 15
});
});
expect(result.current.getRowHeight(0)).toBe(15);
expect(result.current.getRowHeight(1)).toBe(20);
});
test("should unobserve an element when requested", () => {
const { result } = renderHook(() =>
useDynamicRowHeight({
defaultRowHeight: 100
})
);
const element = createRowElement(0);
let unobserve: () => void = NOOP_FUNCTION;
act(() => {
unobserve = result.current.observeRowElements([element]);
setElementSize({
element,
width: 100,
height: 10
});
});
expect(result.current.getRowHeight(0)).toBe(10);
act(() => {
unobserve();
setElementSize({
element,
width: 100,
height: 20
});
});
expect(result.current.getRowHeight(0)).toBe(10);
});
});
// setRowHeight is tested indirectly by "getAverageRowHeight" and "getRowHeight" blocks above
});
================================================
FILE: lib/components/list/useDynamicRowHeight.ts
================================================
import { useCallback, useEffect, useMemo, useState } from "react";
import { useStableCallback } from "../../hooks/useStableCallback";
import { assert } from "../../utils/assert";
import { DATA_ATTRIBUTE_LIST_INDEX } from "./List";
import type { DynamicRowHeight } from "./types";
export function useDynamicRowHeight({
defaultRowHeight,
key
}: {
defaultRowHeight: number;
key?: string | number;
}) {
const [state, setState] = useState<{
key: string | number | undefined;
map: Map<number, number>;
}>({
key,
map: new Map()
});
if (state.key !== key) {
setState({
key,
map: new Map()
});
}
const { map } = state;
const getAverageRowHeight = useCallback(() => {
let totalHeight = 0;
map.forEach((height) => {
totalHeight += height;
});
if (totalHeight === 0) {
return defaultRowHeight;
}
return totalHeight / map.size;
}, [defaultRowHeight, map]);
const getRowHeight = useCallback(
(index: number) => {
const measuredHeight = map.get(index);
if (measuredHeight !== undefined) {
return measuredHeight;
}
// Temporarily store default height in the cache map to avoid scroll jumps if rowProps change
// Else rowProps changes can impact the average height, and cause rows to shift up or down within the list
// see github.com/bvaughn/react-window/issues/863
map.set(index, defaultRowHeight);
return defaultRowHeight;
},
[defaultRowHeight, map]
);
const setRowHeight = useCallback((index: number, size: number) => {
setState((prevState) => {
if (prevState.map.get(index) === size) {
return prevState;
}
const clonedMap = new Map(prevState.map);
clonedMap.set(index, size);
return {
...prevState,
map: clonedMap
};
});
}, []);
const resizeObserverCallback = useStableCallback(
(entries: ResizeObserverEntry[]) => {
if (entries.length === 0) {
return;
}
entries.forEach((entry) => {
const { borderBoxSize, target } = entry;
const attribute = target.getAttribute(DATA_ATTRIBUTE_LIST_INDEX);
assert(
attribute !== null,
`Invalid ${DATA_ATTRIBUTE_LIST_INDEX} attribute value`
);
const index = parseInt(attribute);
const { blockSize: height } = borderBoxSize[0];
if (!height) {
// Ignore heights that have not yet been measured (e.g. <img> elements that have not yet loaded)
return;
}
setRowHeight(index, height);
});
}
);
const [resizeObserver] = useState(() => {
if (typeof ResizeObserver !== "undefined") {
return new ResizeObserver(resizeObserverCallback);
}
});
useEffect(() => {
if (resizeObserver) {
return () => {
resizeObserver.disconnect();
};
}
}, [resizeObserver]);
const observeRowElements = useCallback(
(elements: Element[] | NodeListOf<Element>) => {
if (resizeObserver) {
elements.forEach((element) => resizeObserver.observe(element));
return () => {
elements.forEach((element) => resizeObserver.unobserve(element));
};
}
return () => {};
},
[resizeObserver]
);
return useMemo<DynamicRowHeight>(
() => ({
getAverageRowHeight,
getRowHeight,
setRowHeight,
observeRowElements
}),
[getAverageRowHeight, getRowHeight, setRowHeight, observeRowElements]
);
}
================================================
FILE: lib/components/list/useListCallbackRef.ts
================================================
import { useState } from "react";
import type { ListImperativeAPI } from "./types";
/**
* Convenience hook to return a properly typed ref callback for the List component.
*
* Use this hook when you need to share the ref with another component or hook.
*/
export const useListCallbackRef =
useState as typeof useState<ListImperativeAPI | null>;
================================================
FILE: lib/components/list/useListRef.ts
================================================
import { useRef } from "react";
import type { ListImperativeAPI } from "./types";
/**
* Convenience hook to return a properly typed ref for the List component.
*/
export const useListRef = useRef as typeof useRef<ListImperativeAPI>;
================================================
FILE: lib/core/createCachedBounds.test.ts
================================================
import { describe, expect, test, vi } from "vitest";
import { createCachedBounds } from "./createCachedBounds";
describe("createCachedBounds", () => {
test("should lazily measure items before the requested index", () => {
const itemSize = vi.fn((index: number) => 10 + index);
const cachedBounds = createCachedBounds({
itemCount: 10,
itemProps: {},
itemSize
});
expect(itemSize).not.toHaveBeenCalled();
expect(cachedBounds.size).toBe(0);
expect(cachedBounds.get(2)).toEqual({
scrollOffset: 21,
size: 12
});
expect(itemSize).toHaveBeenCalledTimes(3);
expect(cachedBounds.size).toBe(3);
expect(cachedBounds.get(3)).toEqual({
scrollOffset: 33,
size: 13
});
expect(itemSize).toHaveBeenCalledTimes(4);
expect(cachedBounds.size).toBe(4);
});
test("should cached measured sizes", () => {
const itemSize = vi.fn(() => 10);
const cachedBounds = createCachedBounds({
itemCount: 10,
itemProps: {},
itemSize
});
expect(itemSize).not.toHaveBeenCalled();
expect(cachedBounds.size).toBe(0);
cachedBounds.get(9);
expect(itemSize).toHaveBeenCalledTimes(10);
expect(cachedBounds.size).toBe(10);
for (let index = 0; index < 10; index++) {
cachedBounds.get(index);
}
expect(itemSize).toHaveBeenCalledTimes(10);
expect(cachedBounds.size).toBe(10);
});
test("should gracefully handle an empty cache", () => {
const cachedBounds = createCachedBounds({
itemCount: 0,
itemProps: {},
itemSize: 10
});
expect(cachedBounds.size).toBe(0);
expect(() => {
cachedBounds.get(1);
}).toThrow("Invalid index 1");
});
});
================================================
FILE: lib/core/createCachedBounds.ts
================================================
import { assert } from "../utils/assert";
import type { Bounds, CachedBounds, SizeFunction } from "./types";
export function createCachedBounds<Props extends object>({
itemCount,
itemProps,
itemSize
}: {
itemCount: number;
itemProps: Props;
itemSize: number | SizeFunction<Props>;
}): CachedBounds {
const cache = new Map<number, Bounds>();
return {
get(index: number) {
assert(index < itemCount, `Invalid index ${index}`);
while (cache.size - 1 < index) {
const currentIndex = cache.size;
let size: number;
switch (typeof itemSize) {
case "function": {
size = itemSize(currentIndex, itemProps);
break;
}
case "number": {
size = itemSize;
break;
}
}
if (currentIndex === 0) {
cache.set(currentIndex, {
size,
scrollOffset: 0
});
} else {
const previousRowBounds = cache.get(currentIndex - 1);
assert(
previousRowBounds !== undefined,
`Unexpected bounds cache miss for index ${index}`
);
cache.set(currentIndex, {
scrollOffset:
previousRowBounds.scrollOffset + previousRowBounds.size,
size
});
}
}
const bounds = cache.get(index);
assert(
bounds !== undefined,
`Unexpected bounds cache miss for index ${index}`
);
return bounds;
},
set(index: number, bounds: Bounds) {
cache.set(index, bounds);
},
get size() {
return cache.size;
}
};
}
================================================
FILE: lib/core/getEstimatedSize.test.ts
================================================
import { describe, expect, test } from "vitest";
import { getEstimatedSize } from "./getEstimatedSize";
import { createCachedBounds } from "./createCachedBounds";
import { EMPTY_OBJECT } from "../../src/constants";
describe("getEstimatedSize", () => {
describe("itemSize: function", () => {
const itemSize = (index: number) => 10 + index * 10;
test("should return 0 if no measurements can be taken", () => {
expect(
getEstimatedSize({
cachedBounds: createCachedBounds({
itemCount: 0,
itemProps: EMPTY_OBJECT,
itemSize
}),
itemCount: 0,
itemSize
})
).toBe(0);
});
test("should return an average size based on the first item if no measurements have been taken", () => {
expect(
getEstimatedSize({
cachedBounds: createCachedBounds({
itemCount: 10,
itemProps: EMPTY_OBJECT,
itemSize
}),
itemCount: 10,
itemSize
})
).toBe(100);
});
test("should return estimated size based on averages of what has been measured so far", () => {
const cachedBounds = createCachedBounds({
itemCount: 10,
itemProps: EMPTY_OBJECT,
itemSize
});
cachedBounds.get(4);
expect(
getEstimatedSize({
cachedBounds,
itemCount: 10,
itemSize
})
).toBe(300);
});
test("should return exact size if all content has been measured", () => {
const cachedBounds = createCachedBounds({
itemCount: 10,
itemProps: EMPTY_OBJECT,
itemSize
});
cachedBounds.get(9);
expect(
getEstimatedSize({
cachedBounds,
itemCount: 10,
itemSize
})
).toBe(550);
});
});
describe("itemSize: number", () => {
test("should return exact size even if no measurements have been taken", () => {
expect(
getEstimatedSize({
cachedBounds: createCachedBounds({
itemCount: 10,
itemProps: EMPTY_OBJECT,
itemSize: 25
}),
itemCount: 10,
itemSize: 25
})
).toBe(250);
});
});
});
================================================
FILE: lib/core/getEstimatedSize.ts
================================================
import type { CachedBounds, SizeFunction } from "./types";
import { assert } from "../utils/assert";
export function getEstimatedSize<Props extends object>({
cachedBounds,
itemCount,
itemSize
}: {
cachedBounds: CachedBounds;
itemCount: number;
itemSize: number | SizeFunction<Props>;
}) {
if (itemCount === 0) {
return 0;
} else if (typeof itemSize === "number") {
return itemCount * itemSize;
} else {
const bounds = cachedBounds.get(
cachedBounds.size === 0 ? 0 : cachedBounds.size - 1
);
assert(bounds !== undefined, "Unexpected bounds cache miss");
const averageItemSize =
(bounds.scrollOffset + bounds.size) / cachedBounds.size;
return itemCount * averageItemSize;
}
}
================================================
FILE: lib/core/getOffsetForIndex.test.ts
================================================
import { beforeEach, describe, expect, test } from "vitest";
import { EMPTY_OBJECT } from "../../src/constants";
import type { Align } from "../types";
import { setScrollbarSizeForTests } from "../utils/getScrollbarSize";
import { createCachedBounds } from "./createCachedBounds";
import { getOffsetForIndex } from "./getOffsetForIndex";
describe("getOffsetForIndex", () => {
beforeEach(() => {
setScrollbarSizeForTests(0);
});
// Mimic Size function but with fixed height to simplify tests
const itemSize = () => 10;
type Params = Parameters<typeof getOffsetForIndex>[0];
const DEFAULT_ARGS: Params = {
align: "auto",
cachedBounds: createCachedBounds({
itemCount: 10,
itemProps: EMPTY_OBJECT,
itemSize
}),
containerScrollOffset: 0,
containerSize: 50,
index: 0,
itemCount: 10,
itemSize
};
describe("align", () => {
function createTestHelper(align: Align) {
return function testHelperAuto(
index: number,
expectedOffset: number,
containerScrollOffset: number = 0
) {
expect(
getOffsetForIndex({
...DEFAULT_ARGS,
align,
index,
containerScrollOffset
})
).toBe(expectedOffset);
};
}
test("auto", () => {
const testHelper = createTestHelper("auto");
// Scroll forward
testHelper(0, 0);
testHelper(4, 0);
testHelper(5, 10);
testHelper(9, 50);
// Scroll backward
testHelper(0, 0, 100);
testHelper(4, 40, 100);
});
test("center", () => {
const testHelper = createTestHelper("center");
testHelper(0, 0);
testHelper(1, 0);
testHelper(2, 0);
testHelper(3, 10);
testHelper(4, 20);
testHelper(5, 30);
testHelper(6, 40);
testHelper(7, 50);
testHelper(8, 50);
testHelper(9, 50);
});
test("start", () => {
const testHelper = createTestHelper("start");
testHelper(0, 0);
testHelper(1, 10);
testHelper(2, 20);
testHelper(3, 30);
testHelper(4, 40);
testHelper(4, 40);
testHelper(5, 50);
testHelper(6, 50);
testHelper(7, 50);
testHelper(8, 50);
testHelper(9, 50);
});
test("end", () => {
const testHelper = createTestHelper("end");
testHelper(0, 0);
testHelper(1, 0);
testHelper(2, 0);
testHelper(3, 0);
testHelper(4, 0);
testHelper(4, 0);
testHelper(5, 10);
testHelper(6, 20);
testHelper(7, 30);
testHelper(8, 40);
testHelper(9, 50);
});
test("smart", () => {
const testHelper = createTestHelper("smart");
// Shouldn't scroll if already visible
testHelper(0, 0);
testHelper(3, 0);
testHelper(3, 30, 30);
testHelper(7, 30, 30);
testHelper(7, 50, 50);
testHelper(9, 50, 100);
// Should center align if not visible
testHelper(3, 10, 100);
testHelper(4, 20, 100);
testHelper(6, 40, 0);
testHelper(7, 50, 0);
});
});
});
================================================
FILE: lib/core/getOffsetForIndex.ts
================================================
import type { Align } from "../types";
import { getEstimatedSize } from "./getEstimatedSize";
import type { CachedBounds, SizeFunction } from "./types";
export function getOffsetForIndex<Props extends object>({
align,
cachedBounds,
index,
itemCount,
itemSize,
containerScrollOffset,
containerSize
}: {
align: Align;
cachedBounds: CachedBounds;
index: number;
itemCount: number;
itemSize: number | SizeFunction<Props>;
containerScrollOffset: number;
containerSize: number;
}) {
if (index < 0 || index >= itemCount) {
throw RangeError(`Invalid index specified: ${index}`, {
cause: `Index ${index} is not within the range of 0 - ${itemCount - 1}`
});
}
const estimatedTotalSize = getEstimatedSize({
cachedBounds,
itemCount,
itemSize
});
const bounds = cachedBounds.get(index);
const maxOffset = Math.max(
0,
Math.min(estimatedTotalSize - containerSize, bounds.scrollOffset)
);
const minOffset = Math.max(
0,
bounds.scrollOffset - containerSize + bounds.size
);
if (align === "smart") {
if (
containerScrollOffset >= minOffset &&
containerScrollOffset <= maxOffset
) {
align = "auto";
} else {
align = "center";
}
}
switch (align) {
case "start": {
return maxOffset;
}
case "end": {
return minOffset;
}
case "center": {
if (bounds.scrollOffset <= containerSize / 2) {
// Too near the beginning to center-align
return 0;
} else if (
bounds.scrollOffset + bounds.size / 2 >=
estimatedTotalSize - containerSize / 2
) {
// Too near the end to center-align
return estimatedTotalSize - containerSize;
} else {
return bounds.scrollOffset + bounds.size / 2 - containerSize / 2;
}
}
case "auto":
default: {
if (
containerScrollOffset >= minOffset &&
containerScrollOffset <= maxOffset
) {
return containerScrollOffset;
} else if (containerScrollOffset < minOffset) {
return minOffset;
} else {
return maxOffset;
}
}
}
}
================================================
FILE: lib/core/getStartStopIndices.test.ts
================================================
import { describe, expect, test } from "vitest";
import { createCachedBounds } from "./createCachedBounds";
import { getStartStopIndices } from "./getStartStopIndices";
describe("getStartStopIndices", () => {
function getIndices({
containerScrollOffset,
containerSize,
itemCount,
itemSize,
overscanCount = 0
}: {
containerScrollOffset: number;
containerSize: number;
itemCount: number;
itemSize: number;
overscanCount?: number;
}) {
const cachedBounds = createCachedBounds({
itemCount: itemCount,
itemProps: {},
itemSize
});
return getStartStopIndices({
cachedBounds,
containerScrollOffset,
containerSize,
itemCount,
overscanCount
});
}
test("empty list", () => {
expect(
getIndices({
containerScrollOffset: 0,
containerSize: 100,
itemCount: 0,
itemSize: 25
})
).toEqual({
startIndexVisible: 0,
startIndexOverscan: 0,
stopIndexVisible: -1,
stopIndexOverscan: -1
});
});
test("edge case: not enough rows to fill available height", () => {
expect(
getIndices({
containerScrollOffset: 0,
containerSize: 100,
itemCount: 2,
itemSize: 25
})
).toEqual({
startIndexVisible: 0,
startIndexOverscan: 0,
stopIndexVisible: 1,
stopIndexOverscan: 1
});
});
test("initial set of rows", () => {
expect(
getIndices({
containerScrollOffset: 0,
containerSize: 100,
itemCount: 10,
itemSize: 25
})
).toEqual({
startIndexVisible: 0,
startIndexOverscan: 0,
stopIndexVisible: 3,
stopIndexOverscan: 3
});
});
test("middle set of list", () => {
expect(
getIndices({
containerScrollOffset: 100,
containerSize: 100,
itemCount: 10,
itemSize: 25
})
).toEqual({
startIndexVisible: 4,
startIndexOverscan: 4,
stopIndexVisible: 7,
stopIndexOverscan: 7
});
});
test("final set of rows", () => {
expect(
getIndices({
containerScrollOffset: 150,
containerSize: 100,
itemCount: 10,
itemSize: 25
})
).toEqual({
startIndexVisible: 6,
startIndexOverscan: 6,
stopIndexVisible: 9,
stopIndexOverscan: 9
});
});
test("should not under-scroll", () => {
expect(
getIndices({
containerScrollOffset: -50,
containerSize: 100,
itemCount: 10,
itemSize: 25
})
).toEqual({
startIndexVisible: 0,
startIndexOverscan: 0,
stopIndexVisible: 1,
stopIndexOverscan: 1
});
});
test("should not over-scroll", () => {
expect(
getIndices({
containerScrollOffset: 200,
containerSize: 100,
itemCount: 10,
itemSize: 25
})
).toEqual({
startIndexVisible: 8,
startIndexOverscan: 8,
stopIndexVisible: 9,
stopIndexOverscan: 9
});
});
describe("with overscan", () => {
test("edge case: not enough rows to fill available height", () => {
expect(
getIndices({
containerScrollOffset: 0,
containerSize: 100,
itemCount: 2,
itemSize: 25,
overscanCount: 2
})
).toEqual({
startIndexVisible: 0,
startIndexOverscan: 0,
stopIndexVisible: 1,
stopIndexOverscan: 1
});
});
test("edge case: no rows before", () => {
expect(
getIndices({
containerScrollOffset: 0,
containerSize: 100,
itemCount: 100,
itemSize: 25,
overscanCount: 2
})
).toEqual({
startIndexVisible: 0,
startIndexOverscan: 0,
stopIndexVisible: 3,
stopIndexOverscan: 5
});
});
test("edge case: no rows after", () => {
expect(
getIndices({
containerScrollOffset: 2400,
containerSize: 100,
itemCount: 100,
itemSize: 25,
overscanCount: 2
})
).toEqual({
startIndexVisible: 96,
startIndexOverscan: 94,
stopIndexVisible: 99,
stopIndexOverscan: 99
});
});
test("rows before and after", () => {
expect(
getIndices({
containerScrollOffset: 100,
containerSize: 100,
itemCount: 100,
itemSize: 25,
overscanCount: 2
})
).toEqual({
startIndexVisible: 4,
startIndexOverscan: 2,
stopIndexVisible: 7,
stopIndexOverscan: 9
});
});
});
});
================================================
FILE: lib/core/getStartStopIndices.ts
================================================
import type { CachedBounds } from "./types";
export function getStartStopIndices({
cachedBounds,
containerScrollOffset,
containerSize,
itemCount,
overscanCount
}: {
cachedBounds: CachedBounds;
containerScrollOffset: number;
containerSize: number;
itemCount: number;
overscanCount: number;
}): {
startIndexVisible: number;
stopIndexVisible: number;
startIndexOverscan: number;
stopIndexOverscan: number;
} {
const maxIndex = itemCount - 1;
let startIndexVisible = 0;
let stopIndexVisible = -1;
let startIndexOverscan = 0;
let stopIndexOverscan = -1;
let currentIndex = 0;
while (currentIndex < maxIndex) {
const bounds = cachedBounds.get(currentIndex);
if (bounds.scrollOffset + bounds.size > containerScrollOffset) {
break;
}
currentIndex++;
}
startIndexVisible = currentIndex;
startIndexOverscan = Math.max(0, startIndexVisible - overscanCount);
while (currentIndex < maxIndex) {
const bounds = cachedBounds.get(currentIndex);
if (
bounds.scrollOffset + bounds.size >=
containerScrollOffset + containerSize
) {
break;
}
currentIndex++;
}
stopIndexVisible = Math.min(maxIndex, currentIndex);
stopIndexOverscan = Math.min(itemCount - 1, stopIndexVisible + overscanCount);
if (startIndexVisible < 0) {
startIndexVisible = 0;
stopIndexVisible = -1;
startIndexOverscan = 0;
stopIndexOverscan = -1;
}
return {
startIndexVisible,
stopIndexVisible,
startIndexOverscan,
stopIndexOverscan
};
}
================================================
FILE: lib/core/types.ts
================================================
export type Bounds = {
size: number;
scrollOffset: number;
};
export type CachedBounds = {
get(index: number): Bounds;
set(index: number, bounds: Bounds): void;
size: number;
};
export type Direction = "horizontal" | "vertical";
export type SizeFunction<Props extends object> = (
index: number,
props: Props
) => number;
================================================
FILE: lib/core/useCachedBounds.test.ts
================================================
import {
gitextract_f4nhhmgs/ ├── .github/ │ └── workflows/ │ ├── eslint.yml │ ├── pending-changes.yml │ ├── playwright.yml │ ├── prettier.yml │ ├── typescript.yml │ └── vitest.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .prettierignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── eslint.config.js ├── index.css ├── index.html ├── index.tsx ├── integrations/ │ ├── next/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app/ │ │ │ ├── decoder/ │ │ │ │ └── [encoded]/ │ │ │ │ ├── Decoder.tsx │ │ │ │ └── page.tsx │ │ │ ├── grid/ │ │ │ │ ├── components/ │ │ │ │ │ └── CellComponent.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── list/ │ │ │ │ ├── components/ │ │ │ │ │ └── RowComponent.tsx │ │ │ │ └── page.tsx │ │ │ ├── list-dynamic/ │ │ │ │ ├── components/ │ │ │ │ │ ├── List.tsx │ │ │ │ │ └── RowComponent.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── tailwind.css │ │ ├── eslint.config.mjs │ │ ├── next-env.d.ts │ │ ├── next.config.ts │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ └── tsconfig.json │ ├── tests/ │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── AnimationFrameRowCellCounter.tsx │ │ │ │ ├── DebugData.tsx │ │ │ │ ├── Decoder.tsx │ │ │ │ ├── EnvironmentMarker.tsx │ │ │ │ ├── LayoutShiftDetecter.tsx │ │ │ │ └── RowComponent.tsx │ │ │ ├── index.ts │ │ │ └── utils/ │ │ │ └── serializer/ │ │ │ ├── decode.ts │ │ │ ├── encode.ts │ │ │ └── types.ts │ │ └── tests/ │ │ └── layout-shift.spec.tsx │ ├── vike/ │ │ ├── README.md │ │ ├── package.json │ │ ├── pages/ │ │ │ ├── +Head.tsx │ │ │ ├── +Layout.tsx │ │ │ ├── +config.ts │ │ │ ├── Layout.css │ │ │ ├── _error/ │ │ │ │ └── +Page.tsx │ │ │ ├── decoder/ │ │ │ │ ├── +Page.tsx │ │ │ │ └── +route.ts │ │ │ ├── grid/ │ │ │ │ ├── +Page.tsx │ │ │ │ └── CellComponent.tsx │ │ │ ├── index/ │ │ │ │ └── +Page.tsx │ │ │ ├── list/ │ │ │ │ ├── +Page.tsx │ │ │ │ └── RowComponent.tsx │ │ │ ├── list-dynamic/ │ │ │ │ ├── +Page.tsx │ │ │ │ └── RowComponent.tsx │ │ │ └── tailwind.css │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── vite/ │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── main.tsx │ │ ├── routes/ │ │ │ ├── Decoder.tsx │ │ │ ├── Grid.tsx │ │ │ ├── Home.tsx │ │ │ └── List.tsx │ │ ├── tailwind.css │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── lib/ │ ├── components/ │ │ ├── grid/ │ │ │ ├── Grid.test.tsx │ │ │ ├── Grid.tsx │ │ │ ├── types.ts │ │ │ ├── useGridCallbackRef.ts │ │ │ └── useGridRef.ts │ │ └── list/ │ │ ├── List.test.tsx │ │ ├── List.tsx │ │ ├── isDynamicRowHeight.ts │ │ ├── types.ts │ │ ├── useDynamicRowHeight.test.ts │ │ ├── useDynamicRowHeight.ts │ │ ├── useListCallbackRef.ts │ │ └── useListRef.ts │ ├── core/ │ │ ├── createCachedBounds.test.ts │ │ ├── createCachedBounds.ts │ │ ├── getEstimatedSize.test.ts │ │ ├── getEstimatedSize.ts │ │ ├── getOffsetForIndex.test.ts │ │ ├── getOffsetForIndex.ts │ │ ├── getStartStopIndices.test.ts │ │ ├── getStartStopIndices.ts │ │ ├── types.ts │ │ ├── useCachedBounds.test.ts │ │ ├── useCachedBounds.ts │ │ ├── useIsRtl.ts │ │ ├── useItemSize.ts │ │ ├── useVirtualizer.test.ts │ │ └── useVirtualizer.ts │ ├── hooks/ │ │ ├── useIsomorphicLayoutEffect.ts │ │ ├── useMemoizedObject.test.ts │ │ ├── useMemoizedObject.ts │ │ ├── useResizeObserver.test.ts │ │ ├── useResizeObserver.ts │ │ ├── useStableCallback.test.tsx │ │ └── useStableCallback.ts │ ├── index.ts │ ├── types.ts │ └── utils/ │ ├── adjustScrollOffsetForRtl.ts │ ├── areArraysEqual.ts │ ├── arePropsEqual.ts │ ├── assert.ts │ ├── colors/ │ │ ├── getContrastColor.ts │ │ └── stringToColor.ts │ ├── debug.ts │ ├── getRTLOffsetType.ts │ ├── getScrollbarSize.ts │ ├── isRtl.ts │ ├── parseNumericStyleValue.test.ts │ ├── parseNumericStyleValue.ts │ ├── shallowCompare.test.ts │ ├── shallowCompare.ts │ └── test/ │ ├── mockResizeObserver.ts │ └── mockScrollTo.ts ├── package.json ├── pnpm-workspace.yaml ├── postcss.config.js ├── prettier.config.js ├── public/ │ ├── data/ │ │ ├── addresses.json │ │ ├── contacts.json │ │ ├── lorem.json │ │ └── names.json │ ├── generated/ │ │ ├── docs/ │ │ │ ├── Grid.json │ │ │ ├── GridImperativeAPI.json │ │ │ ├── List.json │ │ │ └── ListImperativeAPI.json │ │ ├── examples/ │ │ │ ├── BasicRow.json │ │ │ ├── CellComponent.json │ │ │ ├── CellComponentAriaRoles.json │ │ │ ├── FixedHeightList.json │ │ │ ├── FixedHeightRowComponent.json │ │ │ ├── FlexboxLayout.json │ │ │ ├── Grid.json │ │ │ ├── GridAriaRoles.json │ │ │ ├── HorizontalList.json │ │ │ ├── HorizontalListCellRenderer.json │ │ │ ├── ImageRow.json │ │ │ ├── Images.json │ │ │ ├── ListAriaRoles.json │ │ │ ├── ListDynamicRowHeights.json │ │ │ ├── ListRowDynamicRowHeights.json │ │ │ ├── ListVariableRowHeights.json │ │ │ ├── ListWithStickyRows.json │ │ │ ├── RefComposition.json │ │ │ ├── RowComponentAriaRoles.json │ │ │ ├── RtlGrid.json │ │ │ ├── ScrollingIndicator.json │ │ │ ├── TableAriaAttributes.json │ │ │ ├── TableAriaOverrideProps.json │ │ │ ├── columnWidth.json │ │ │ ├── gridRefClickEventHandler.json │ │ │ ├── listRefClickEventHandler.json │ │ │ ├── rowHeight.json │ │ │ ├── shared.json │ │ │ ├── useGridCallbackRef.json │ │ │ ├── useGridRef.json │ │ │ ├── useGridRefImport.json │ │ │ ├── useListCallbackRef.json │ │ │ ├── useListRef.json │ │ │ └── useListRefImport.json │ │ ├── search-index.json │ │ └── search-records.json │ └── robots.txt ├── scripts/ │ ├── compile-docs.ts │ ├── compile-examples.ts │ ├── compile-search-index.ts │ └── compress-og-image.ts ├── src/ │ ├── App.tsx │ ├── components/ │ │ ├── ContinueLink.tsx │ │ ├── Link.tsx │ │ └── NavLink.tsx │ ├── constants.ts │ ├── hooks/ │ │ └── useLocalStorage.ts │ ├── routes/ │ │ ├── HowDoesItWorkRoute.tsx │ │ ├── PlatformRequirementsRoute.tsx │ │ ├── ScratchpadRoute.tsx │ │ ├── examples/ │ │ │ ├── BasicRow.tsx │ │ │ ├── RefComposition.tsx │ │ │ └── ScrollingIndicator.tsx │ │ ├── grid/ │ │ │ ├── AriaRolesRoute.tsx │ │ │ ├── HorizontalListsRoute.tsx │ │ │ ├── ImperativeHandleRoute.tsx │ │ │ ├── PropsRoute.tsx │ │ │ ├── RTLGridsRoute.tsx │ │ │ ├── RenderingGridRoute.tsx │ │ │ ├── ScrollToCellRoute.tsx │ │ │ ├── examples/ │ │ │ │ ├── CellComponent.tsx │ │ │ │ ├── CellComponentAriaRoles.tsx │ │ │ │ ├── Grid.tsx │ │ │ │ ├── GridAriaRoles.html │ │ │ │ ├── HorizontalList.tsx │ │ │ │ ├── HorizontalListCellRenderer.tsx │ │ │ │ ├── RtlGrid.tsx │ │ │ │ ├── columnWidth.ts │ │ │ │ ├── gridRefClickEventHandler.ts │ │ │ │ ├── shared.ts │ │ │ │ ├── useGridCallbackRef.tsx │ │ │ │ ├── useGridRef.tsx │ │ │ │ └── useGridRefImport.ts │ │ │ └── hooks/ │ │ │ ├── useContacts.ts │ │ │ └── useEmails.ts │ │ ├── list/ │ │ │ ├── AriaRolesRoute.tsx │ │ │ ├── DynamicRowHeightsRoute.tsx │ │ │ ├── FixedRowHeightsRoute.tsx │ │ │ ├── ImagesRoute.tsx │ │ │ ├── ImperativeApiRoute.tsx │ │ │ ├── PropsRoute.tsx │ │ │ ├── ScrollToRowRoute.tsx │ │ │ ├── StickyRowsRoute.tsx │ │ │ ├── VariableRowHeightsRoute.tsx │ │ │ ├── examples/ │ │ │ │ ├── FixedHeightList.tsx │ │ │ │ ├── FixedHeightRowComponent.tsx │ │ │ │ ├── ImageRow.tsx │ │ │ │ ├── Images.tsx │ │ │ │ ├── ListAriaRoles.html │ │ │ │ ├── ListDynamicRowHeights.tsx │ │ │ │ ├── ListRowDynamicRowHeights.tsx │ │ │ │ ├── ListVariableRowHeights.tsx │ │ │ │ ├── ListWithStickyRows.tsx │ │ │ │ ├── RowComponentAriaRoles.tsx │ │ │ │ ├── listRefClickEventHandler.ts │ │ │ │ ├── rowHeight.ts │ │ │ │ ├── useListCallbackRef.tsx │ │ │ │ ├── useListRef.tsx │ │ │ │ └── useListRefImport.ts │ │ │ └── hooks/ │ │ │ ├── useCitiesByState.ts │ │ │ └── useLorem.ts │ │ └── tables/ │ │ ├── AriaRolesRoute.tsx │ │ ├── TabularDataRoute.tsx │ │ ├── examples/ │ │ │ ├── FlexboxLayout.tsx │ │ │ ├── TableAriaAttributes.html │ │ │ └── TableAriaOverrideProps.tsx │ │ └── hooks/ │ │ └── useAddresses.ts │ ├── routes.ts │ └── vite-env.d.ts ├── temp.TODO.md ├── tsconfig.json ├── vercel.json ├── vite.config.ts ├── vitest.config.ts └── vitest.setup.js
SYMBOL INDEX (240 symbols across 132 files)
FILE: integrations/next/app/decoder/[encoded]/Decoder.tsx
function Decoder (line 6) | function Decoder({
FILE: integrations/next/app/decoder/[encoded]/page.tsx
function Page (line 3) | async function Page({
FILE: integrations/next/app/grid/components/CellComponent.tsx
function CellComponent (line 5) | function CellComponent({
FILE: integrations/next/app/grid/page.tsx
function Home (line 9) | async function Home() {
FILE: integrations/next/app/layout.tsx
function RootLayout (line 22) | function RootLayout({
FILE: integrations/next/app/list-dynamic/components/List.tsx
function List (line 6) | function List() {
FILE: integrations/next/app/list-dynamic/components/RowComponent.tsx
function RowComponent (line 5) | function RowComponent({
FILE: integrations/next/app/list-dynamic/page.tsx
function Home (line 8) | async function Home() {
FILE: integrations/next/app/list/components/RowComponent.tsx
function RowComponent (line 5) | function RowComponent({
FILE: integrations/next/app/list/page.tsx
function Home (line 9) | async function Home() {
FILE: integrations/next/app/page.tsx
function Home (line 3) | async function Home() {
FILE: integrations/tests/src/components/AnimationFrameRowCellCounter.tsx
function AnimationFrameRowCellCounter (line 5) | function AnimationFrameRowCellCounter() {
FILE: integrations/tests/src/components/DebugData.tsx
function DebugData (line 3) | function DebugData({ data }: { data: object }) {
function replacer (line 16) | function replacer(_key: string, value: unknown) {
FILE: integrations/tests/src/components/Decoder.tsx
function Decoder (line 8) | function Decoder({
FILE: integrations/tests/src/components/EnvironmentMarker.tsx
function EnvironmentMarker (line 1) | function EnvironmentMarker({ children }: { children: string }) {
FILE: integrations/tests/src/components/LayoutShiftDetecter.tsx
type PerformanceEntry (line 5) | type PerformanceEntry = {
function LayoutShiftDetecter (line 11) | function LayoutShiftDetecter() {
FILE: integrations/tests/src/components/RowComponent.tsx
type RowComponentData (line 3) | type RowComponentData = {
function RowComponent (line 7) | function RowComponent({
FILE: integrations/tests/src/utils/serializer/decode.ts
function decode (line 17) | function decode(stringified: string) {
function decodeChildren (line 23) | function decodeChildren(children: EncodedElement[]): ReactElement<unknow...
function decodeList (line 53) | function decodeList(
function decodeRowComponent (line 62) | function decodeRowComponent(
function decodeText (line 71) | function decodeText(json: EncodedTextElement): ReactElement<TextProps> {
FILE: integrations/tests/src/utils/serializer/encode.ts
function encode (line 15) | function encode(element: ReactElement<unknown>) {
function encodeChildren (line 22) | function encodeChildren(children: ReactElement<unknown>[]): EncodedEleme...
function encodeList (line 61) | function encodeList(
function encodeRowComponent (line 70) | function encodeRowComponent(
function encodeTextChild (line 79) | function encodeTextChild(element: ReactElement<TextProps>): EncodedTextE...
FILE: integrations/tests/src/utils/serializer/types.ts
type EncodedListElement (line 4) | interface EncodedListElement {
type EncodedRowComponentElement (line 9) | interface EncodedRowComponentElement {
type TextProps (line 14) | type TextProps = {
type EncodedTextElement (line 19) | interface EncodedTextElement {
type EncodedElement (line 24) | type EncodedElement =
FILE: integrations/tests/tests/layout-shift.spec.tsx
function assertRenderedBy (line 5) | async function assertRenderedBy(page: Page, type: "client" | "server") {
FILE: integrations/vike/pages/+Head.tsx
function Head (line 3) | function Head() {
FILE: integrations/vike/pages/+Layout.tsx
function Layout (line 5) | function Layout({ children }: { children: React.ReactNode }) {
FILE: integrations/vike/pages/_error/+Page.tsx
function Page (line 3) | function Page() {
FILE: integrations/vike/pages/decoder/+Page.tsx
function Page (line 5) | function Page() {
FILE: integrations/vike/pages/grid/+Page.tsx
function Page (line 9) | function Page() {
FILE: integrations/vike/pages/grid/CellComponent.tsx
function CellComponent (line 3) | function CellComponent({
FILE: integrations/vike/pages/index/+Page.tsx
function Page (line 1) | function Page() {
FILE: integrations/vike/pages/list-dynamic/+Page.tsx
function Page (line 9) | function Page() {
FILE: integrations/vike/pages/list-dynamic/RowComponent.tsx
function RowComponent (line 3) | function RowComponent({
FILE: integrations/vike/pages/list/+Page.tsx
function Page (line 9) | function Page() {
FILE: integrations/vike/pages/list/RowComponent.tsx
function RowComponent (line 3) | function RowComponent({
FILE: integrations/vite/src/routes/Decoder.tsx
function DecoderRoute (line 4) | function DecoderRoute() {
FILE: integrations/vite/src/routes/Grid.tsx
function GridRoute (line 8) | function GridRoute() {
function CellComponent (line 30) | function CellComponent({
FILE: integrations/vite/src/routes/Home.tsx
function HomeRoute (line 3) | function HomeRoute() {
FILE: integrations/vite/src/routes/List.tsx
function ListRoute (line 8) | function ListRoute() {
function RowComponent (line 27) | function RowComponent({
FILE: lib/components/grid/Grid.test.tsx
function CustomCellComponent (line 351) | function CustomCellComponent({ style }: CellComponentProps<object>) {
function CellComponentWithCellProps (line 602) | function CellComponentWithCellProps({
function Test (line 614) | function Test() {
FILE: lib/components/grid/Grid.tsx
function Grid (line 26) | function Grid<
FILE: lib/components/grid/types.ts
type ForbiddenKeys (line 11) | type ForbiddenKeys = "ariaAttributes" | "columnIndex" | "rowIndex" | "st...
type ExcludeForbiddenKeys (line 12) | type ExcludeForbiddenKeys<Type> = {
type GridProps (line 16) | type GridProps<
type CellComponent (line 219) | type CellComponent<CellProps extends object> =
type CellComponentProps (line 221) | type CellComponentProps<CellProps extends object = object> =
type ScrollState (line 224) | type ScrollState = {
type OnCellsRendered (line 229) | type OnCellsRendered = NonNullable<GridProps<object>["onCellsRendered"]>;
type CachedBounds (line 231) | type CachedBounds = Map<
type GridImperativeAPI (line 246) | interface GridImperativeAPI {
FILE: lib/components/list/List.test.tsx
function CustomRowComponent (line 386) | function CustomRowComponent({ index, style }: RowComponentProps<object>) {
function Example (line 588) | function Example() {
function setMockRowHeights (line 605) | function setMockRowHeights(
function CustomRowComponent (line 792) | function CustomRowComponent({
function RowComponentWithRowProps (line 828) | function RowComponentWithRowProps({
function Test (line 835) | function Test() {
FILE: lib/components/list/List.tsx
constant DATA_ATTRIBUTE_LIST_INDEX (line 21) | const DATA_ATTRIBUTE_LIST_INDEX = "data-react-window-index";
function List (line 26) | function List<
FILE: lib/components/list/isDynamicRowHeight.ts
function isDynamicRowHeight (line 3) | function isDynamicRowHeight(value: unknown): value is DynamicRowHeight {
FILE: lib/components/list/types.ts
type DynamicRowHeight (line 11) | type DynamicRowHeight = {
type ForbiddenKeys (line 18) | type ForbiddenKeys = "ariaAttributes" | "index" | "style";
type ExcludeForbiddenKeys (line 19) | type ExcludeForbiddenKeys<Type> = {
type ListProps (line 23) | type ListProps<
type RowComponent (line 160) | type RowComponent<RowProps extends object> =
type RowComponentProps (line 163) | type RowComponentProps<RowProps extends object = object> =
type OnRowsRendered (line 166) | type OnRowsRendered = NonNullable<ListProps<object>["onRowsRendered"]>;
type CachedBounds (line 168) | type CachedBounds = Map<
type ListImperativeAPI (line 181) | interface ListImperativeAPI {
FILE: lib/components/list/useDynamicRowHeight.test.ts
function createRowElement (line 121) | function createRowElement(index: number) {
FILE: lib/components/list/useDynamicRowHeight.ts
function useDynamicRowHeight (line 7) | function useDynamicRowHeight({
FILE: lib/core/createCachedBounds.ts
function createCachedBounds (line 4) | function createCachedBounds<Props extends object>({
FILE: lib/core/getEstimatedSize.ts
function getEstimatedSize (line 4) | function getEstimatedSize<Props extends object>({
FILE: lib/core/getOffsetForIndex.test.ts
type Params (line 16) | type Params = Parameters<typeof getOffsetForIndex>[0];
function createTestHelper (line 32) | function createTestHelper(align: Align) {
FILE: lib/core/getOffsetForIndex.ts
function getOffsetForIndex (line 5) | function getOffsetForIndex<Props extends object>({
FILE: lib/core/getStartStopIndices.test.ts
function getIndices (line 6) | function getIndices({
FILE: lib/core/getStartStopIndices.ts
function getStartStopIndices (line 3) | function getStartStopIndices({
FILE: lib/core/types.ts
type Bounds (line 1) | type Bounds = {
type CachedBounds (line 6) | type CachedBounds = {
type Direction (line 12) | type Direction = "horizontal" | "vertical";
type SizeFunction (line 14) | type SizeFunction<Props extends object> = (
FILE: lib/core/useCachedBounds.ts
function useCachedBounds (line 5) | function useCachedBounds<Props extends object>({
FILE: lib/core/useIsRtl.ts
function useIsRtl (line 4) | function useIsRtl(
FILE: lib/core/useItemSize.ts
function useItemSize (line 4) | function useItemSize<Props extends object>({
FILE: lib/core/useVirtualizer.ts
function useVirtualizer (line 21) | function useVirtualizer<Props extends object>({
FILE: lib/hooks/useMemoizedObject.ts
function useMemoizedObject (line 3) | function useMemoizedObject<Type extends object>(
FILE: lib/hooks/useResizeObserver.ts
function useResizeObserver (line 5) | function useResizeObserver({
FILE: lib/hooks/useStableCallback.ts
function useStableCallback (line 5) | function useStableCallback<Args, Return>(
FILE: lib/types.ts
type Align (line 3) | type Align = "auto" | "center" | "end" | "smart" | "start";
type TagNames (line 5) | type TagNames = keyof JSX.IntrinsicElements;
FILE: lib/utils/adjustScrollOffsetForRtl.ts
function adjustScrollOffsetForRtl (line 4) | function adjustScrollOffsetForRtl({
FILE: lib/utils/areArraysEqual.ts
function areArraysEqual (line 1) | function areArraysEqual(a: unknown[], b: unknown[]) {
FILE: lib/utils/arePropsEqual.ts
function arePropsEqual (line 7) | function arePropsEqual(
FILE: lib/utils/assert.ts
function assert (line 1) | function assert(
FILE: lib/utils/colors/getContrastColor.ts
function getContrastColor (line 1) | function getContrastColor(hex: string) {
FILE: lib/utils/colors/stringToColor.ts
function stringToColor (line 1) | function stringToColor(string: string) {
FILE: lib/utils/debug.ts
function debug (line 4) | function debug(namespace: string, ...args: unknown[]) {
FILE: lib/utils/getRTLOffsetType.ts
type RTLOffsetType (line 1) | type RTLOffsetType =
function getRTLOffsetType (line 14) | function getRTLOffsetType(recalculate: boolean = false): RTLOffsetType {
FILE: lib/utils/getScrollbarSize.ts
function getScrollbarSize (line 3) | function getScrollbarSize(recalculate: boolean = false): number {
function setScrollbarSizeForTests (line 21) | function setScrollbarSizeForTests(value: number) {
FILE: lib/utils/isRtl.ts
function isRtl (line 1) | function isRtl(element: HTMLElement) {
FILE: lib/utils/parseNumericStyleValue.ts
function parseNumericStyleValue (line 3) | function parseNumericStyleValue(
FILE: lib/utils/shallowCompare.ts
function shallowCompare (line 3) | function shallowCompare<Type extends object>(
FILE: lib/utils/test/mockResizeObserver.ts
type GetDOMRect (line 3) | type GetDOMRect = (element: HTMLElement) => DOMRectReadOnly | undefined ...
function disableResizeObserverForCurrentTest (line 14) | function disableResizeObserverForCurrentTest() {
function setDefaultElementSize (line 18) | function setDefaultElementSize({
function setElementSizeFunction (line 30) | function setElementSizeFunction(value: GetDOMRect) {
function setElementSize (line 36) | function setElementSize({
function simulateUnsupportedEnvironmentForTest (line 50) | function simulateUnsupportedEnvironmentForTest() {
function mockResizeObserver (line 55) | function mockResizeObserver() {
class MockResizeObserver (line 73) | class MockResizeObserver implements ResizeObserver {
method constructor (line 78) | constructor(callback: ResizeObserverCallback) {
method observe (line 84) | observe(element: HTMLElement) {
method unobserve (line 93) | unobserve(element: HTMLElement) {
method disconnect (line 97) | disconnect() {
method #notify (line 104) | #notify(elements: HTMLElement[]) {
FILE: lib/utils/test/mockScrollTo.ts
function mockScrollTo (line 3) | function mockScrollTo() {
FILE: src/App.tsx
function App (line 15) | function App() {
constant VERSIONS (line 143) | const VERSIONS = {
FILE: src/components/ContinueLink.tsx
function ContinueLink (line 4) | function ContinueLink({ title, to }: { title: string; to: Path }) {
FILE: src/components/Link.tsx
function Link (line 5) | function Link({
FILE: src/components/NavLink.tsx
function NavLink (line 5) | function NavLink({
FILE: src/constants.ts
constant EMPTY_ARRAY (line 1) | const EMPTY_ARRAY: unknown[] = [];
constant EMPTY_OBJECT (line 2) | const EMPTY_OBJECT = {};
FILE: src/hooks/useLocalStorage.ts
function useLocalStorage (line 3) | function useLocalStorage<Type>(
FILE: src/routes.ts
type Route (line 3) | type Route = LazyExoticComponent<ComponentType<unknown>>;
type Routes (line 45) | type Routes = Record<keyof typeof routes, Route>;
type Path (line 46) | type Path = keyof Routes;
FILE: src/routes/HowDoesItWorkRoute.tsx
function HowDoesItWorkRoute (line 15) | function HowDoesItWorkRoute() {
function List (line 88) | function List({
function Row (line 116) | function Row({
function Viewport (line 137) | function Viewport({
FILE: src/routes/PlatformRequirementsRoute.tsx
function PlatformRequirementsRoute (line 3) | function PlatformRequirementsRoute() {
FILE: src/routes/ScratchpadRoute.tsx
type Item (line 9) | type Item = {
function ScratchpadRoute (line 16) | function ScratchpadRoute() {
function Row (line 79) | function Row({
function createItems (line 110) | function createItems() {
function Button (line 127) | function Button({
FILE: src/routes/examples/BasicRow.tsx
function Row (line 9) | function Row({ index, style }: RowComponentProps) {
FILE: src/routes/grid/AriaRolesRoute.tsx
function AriaRolesRoute (line 6) | function AriaRolesRoute() {
FILE: src/routes/grid/HorizontalListsRoute.tsx
function HorizontalListsRoute (line 7) | function HorizontalListsRoute() {
FILE: src/routes/grid/ImperativeHandleRoute.tsx
function GridImperativeHandleRoute (line 12) | function GridImperativeHandleRoute() {
FILE: src/routes/grid/PropsRoute.tsx
function GridPropsRoute (line 4) | function GridPropsRoute() {
FILE: src/routes/grid/RTLGridsRoute.tsx
function RTLGridsRoute (line 13) | function RTLGridsRoute() {
FILE: src/routes/grid/RenderingGridRoute.tsx
function RenderingGridRoute (line 17) | function RenderingGridRoute() {
FILE: src/routes/grid/ScrollToCellRoute.tsx
constant EMPTY_OPTION (line 25) | const EMPTY_OPTION: Option<string> = {
constant ALIGNMENTS (line 30) | const ALIGNMENTS: Option<Align>[] = (
constant BEHAVIORS (line 38) | const BEHAVIORS: Option<ScrollBehavior>[] = (
constant COLUMNS (line 46) | const COLUMNS: Option<string>[] = COLUMN_KEYS.map((key) => ({
function ScrollToCellRoute (line 51) | function ScrollToCellRoute() {
FILE: src/routes/grid/examples/CellComponent.tsx
function CellComponent (line 8) | function CellComponent({
FILE: src/routes/grid/examples/CellComponentAriaRoles.tsx
function CellComponent (line 3) | function CellComponent({
FILE: src/routes/grid/examples/Grid.tsx
type Contact (line 5) | type Contact = (typeof json)[0];
function Example (line 11) | function Example({ contacts }: { contacts: Contact[] }) {
FILE: src/routes/grid/examples/HorizontalList.tsx
function HorizontalList (line 7) | function HorizontalList({ emails }: { emails: string[] }) {
FILE: src/routes/grid/examples/HorizontalListCellRenderer.tsx
function CellComponent (line 7) | function CellComponent({
FILE: src/routes/grid/examples/RtlGrid.tsx
type Contact (line 4) | type Contact = (typeof json)[0];
function RtlExample (line 10) | function RtlExample({ contacts }: { contacts: Contact[] }) {
function CellComponent (line 29) | function CellComponent({
FILE: src/routes/grid/examples/columnWidth.ts
function columnWidth (line 5) | function columnWidth(index: number) {
FILE: src/routes/grid/examples/shared.ts
constant COLUMN_KEYS (line 3) | const COLUMN_KEYS: (keyof Contact)[] = [
function indexToColumn (line 18) | function indexToColumn(columnIndex: number): keyof Contact {
FILE: src/routes/grid/examples/useGridCallbackRef.tsx
type Props (line 3) | type Props = GridProps<object>;
function useCustomHook (line 5) | function useCustomHook(ref: GridImperativeAPI | null) {
function Example (line 13) | function Example(props: Props) {
FILE: src/routes/grid/examples/useGridRef.tsx
type Props (line 3) | type Props = GridProps<object>;
function Example (line 7) | function Example(props: Props) {
FILE: src/routes/grid/hooks/useContacts.ts
type Contact (line 4) | type Contact = (typeof json)[0];
function useContacts (line 6) | function useContacts(): Contact[] {
FILE: src/routes/grid/hooks/useEmails.ts
function useEmails (line 4) | function useEmails(): string[] {
FILE: src/routes/list/AriaRolesRoute.tsx
function AriaRolesRoute (line 6) | function AriaRolesRoute() {
FILE: src/routes/list/DynamicRowHeightsRoute.tsx
function DynamicRowHeightsRoute (line 15) | function DynamicRowHeightsRoute() {
FILE: src/routes/list/FixedRowHeightsRoute.tsx
function FixedRowHeightsRoute (line 15) | function FixedRowHeightsRoute() {
FILE: src/routes/list/ImagesRoute.tsx
function ImagesRoute (line 7) | function ImagesRoute() {
FILE: src/routes/list/ImperativeApiRoute.tsx
function ListImperativeApiRoute (line 12) | function ListImperativeApiRoute() {
FILE: src/routes/list/PropsRoute.tsx
function ListPropsRoute (line 4) | function ListPropsRoute() {
FILE: src/routes/list/ScrollToRowRoute.tsx
constant EMPTY_OPTION (line 23) | const EMPTY_OPTION: Option<string> = {
constant ALIGNMENTS (line 28) | const ALIGNMENTS: Option<Align>[] = (
constant BEHAVIORS (line 36) | const BEHAVIORS: Option<ScrollBehavior>[] = (
function ScrollToRowRoute (line 44) | function ScrollToRowRoute() {
FILE: src/routes/list/StickyRowsRoute.tsx
function StickyRowsRoute (line 12) | function StickyRowsRoute() {
FILE: src/routes/list/VariableRowHeightsRoute.tsx
function VariableRowHeightsRoute (line 7) | function VariableRowHeightsRoute() {
FILE: src/routes/list/examples/FixedHeightList.tsx
function Example (line 7) | function Example({ names }: { names: string[] }) {
FILE: src/routes/list/examples/FixedHeightRowComponent.tsx
function RowComponent (line 3) | function RowComponent({
FILE: src/routes/list/examples/ImageRow.tsx
type RowProps (line 7) | type RowProps = {
function RowComponent (line 11) | function RowComponent({ index, images, style }: RowComponentProps<RowPro...
function LoadingSpinner (line 24) | function LoadingSpinner({ className }: { className: string }) {
FILE: src/routes/list/examples/Images.tsx
type Size (line 3) | type Size = {
function Example (line 12) | function Example({ images }: { images: string[] }) {
constant IMAGES (line 29) | const IMAGES: string[] = [
function ExampleWithImages (line 58) | function ExampleWithImages() {
FILE: src/routes/list/examples/ListDynamicRowHeights.tsx
function Example (line 7) | function Example({ lorem }: { lorem: string[] }) {
type ListState (line 26) | type ListState = {
function useListState (line 32) | function useListState(lorem: string[]) {
FILE: src/routes/list/examples/ListRowDynamicRowHeights.tsx
function RowComponent (line 9) | function RowComponent({
function ToggleIcon (line 37) | function ToggleIcon({ isCollapsed }: { isCollapsed: boolean }) {
FILE: src/routes/list/examples/ListVariableRowHeights.tsx
type Item (line 11) | type Item =
type RowProps (line 15) | type RowProps = {
function Example (line 19) | function Example({ items }: { items: Item[] }) {
function rowHeight (line 30) | function rowHeight(index: number, { items }: RowProps) {
function RowComponent (line 43) | function RowComponent({ index, items, style }: RowComponentProps<RowProp...
function ExampleWithRef (line 65) | function ExampleWithRef({
FILE: src/routes/list/examples/ListWithStickyRows.tsx
function RowComponent (line 3) | function RowComponent({ index, style }: RowComponentProps<object>) {
function Example (line 15) | function Example() {
FILE: src/routes/list/examples/RowComponentAriaRoles.tsx
function RowComponent (line 3) | function RowComponent({
FILE: src/routes/list/examples/rowHeight.ts
function rowHeight (line 5) | function rowHeight(index: number, { items }: RowProps) {
FILE: src/routes/list/examples/useListCallbackRef.tsx
type Props (line 3) | type Props = ListProps<object>;
function useCustomHook (line 5) | function useCustomHook(ref: ListImperativeAPI | null) {
function Example (line 13) | function Example(props: Props) {
FILE: src/routes/list/examples/useListRef.tsx
type Props (line 3) | type Props = ListProps<object>;
function Example (line 7) | function Example(props: Props) {
FILE: src/routes/list/hooks/useCitiesByState.ts
type Item (line 4) | type Item =
function useCitiesByState (line 8) | function useCitiesByState(): Item[] {
FILE: src/routes/list/hooks/useLorem.ts
function useLorem (line 3) | function useLorem() {
FILE: src/routes/tables/AriaRolesRoute.tsx
function AriaRolesRoute (line 5) | function AriaRolesRoute() {
FILE: src/routes/tables/TabularDataRoute.tsx
function TabularDataRoute (line 15) | function TabularDataRoute() {
FILE: src/routes/tables/examples/FlexboxLayout.tsx
type Address (line 4) | type Address = (typeof json)[0];
function Example (line 10) | function Example({ addresses }: { addresses: Address[] }) {
function RowComponent (line 35) | function RowComponent({
FILE: src/routes/tables/examples/TableAriaOverrideProps.tsx
function Example (line 12) | function Example() {
function RowComponent (line 32) | function RowComponent({ index, style }: RowComponentProps<object>) {
FILE: src/routes/tables/hooks/useAddresses.ts
type Address (line 4) | type Address = (typeof json)[0];
function useAddresses (line 6) | function useAddresses(): Address[] {
Condensed preview — 262 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,423K chars).
[
{
"path": ".github/workflows/eslint.yml",
"chars": 368,
"preview": "name: \"ESLint\"\non: [pull_request]\njobs:\n eslint:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v"
},
{
"path": ".github/workflows/pending-changes.yml",
"chars": 761,
"preview": "name: \"Pending changes\"\non: [pull_request]\njobs:\n pending-changes:\n runs-on: ubuntu-latest\n steps:\n - uses: "
},
{
"path": ".github/workflows/playwright.yml",
"chars": 1176,
"preview": "name: \"Playwright Tests\"\non: [pull_request]\njobs:\n e2e-tests:\n runs-on: ubuntu-latest\n steps:\n - uses: actio"
},
{
"path": ".github/workflows/prettier.yml",
"chars": 385,
"preview": "name: \"Prettier\"\non: [pull_request]\njobs:\n prettier:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checko"
},
{
"path": ".github/workflows/typescript.yml",
"chars": 435,
"preview": "name: \"TypeScript\"\non: [pull_request]\njobs:\n typescript:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/ch"
},
{
"path": ".github/workflows/vitest.yml",
"chars": 439,
"preview": "name: \"Vitest\"\non: [pull_request]\njobs:\n unit-tests:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checko"
},
{
"path": ".gitignore",
"chars": 284,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n/dist\n/d"
},
{
"path": ".husky/pre-commit",
"chars": 22,
"preview": "pnpm exec lint-staged\n"
},
{
"path": ".prettierignore",
"chars": 65,
"preview": "/dist\n/docs\n/generated\n/public\n\nREADME.md\nintegrations/next/.next"
},
{
"path": "CHANGELOG.md",
"chars": 21047,
"preview": "# Changelog\n\n## 2.2.7\n\n- Fixed a problem with project logo not displaying correctly in the README for the Firefox browse"
},
{
"path": "CONTRIBUTING.md",
"chars": 1019,
"preview": "# Contributing\n\nThanks for your interest in contributing to this project!\n\nHere are a couple of guidelines to keep in mi"
},
{
"path": "LICENSE.md",
"chars": 1079,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2018 Brian Vaughn\n\nPermission is hereby granted, free of charge, to any person obta"
},
{
"path": "README.md",
"chars": 19125,
"preview": "<img src=\"https://react-window.vercel.app/og.png\" alt=\"react-window logo\" width=\"400\" height=\"210\" />\n\n`react-window` is"
},
{
"path": "eslint.config.js",
"chars": 688,
"preview": "import js from \"@eslint/js\";\nimport globals from \"globals\";\nimport reactHooks from \"eslint-plugin-react-hooks\";\nimport r"
},
{
"path": "index.css",
"chars": 475,
"preview": "@source \"node_modules/react-lib-tools\";\n\n@import \"tailwindcss\";\n@import \"react-lib-tools/styles.css\";\n\n@theme {\n --colo"
},
{
"path": "index.html",
"chars": 1007,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <title>react-window | render everything</title>\n\n <link rel=\"icon\" type"
},
{
"path": "index.tsx",
"chars": 238,
"preview": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport \"./index.css\";\nimport App from"
},
{
"path": "integrations/next/.gitignore",
"chars": 5,
"preview": ".next"
},
{
"path": "integrations/next/README.md",
"chars": 1450,
"preview": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-re"
},
{
"path": "integrations/next/app/decoder/[encoded]/Decoder.tsx",
"chars": 649,
"preview": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Decoder as DecoderExternal } from \"../../../../tests\";\n\nexport"
},
{
"path": "integrations/next/app/decoder/[encoded]/page.tsx",
"chars": 385,
"preview": "import { Decoder } from \"./Decoder\";\n\nexport default async function Page({\n params,\n searchParams: searchParamsPromise"
},
{
"path": "integrations/next/app/grid/components/CellComponent.tsx",
"chars": 336,
"preview": "\"use client\";\n\nimport { type CellComponentProps } from \"react-window\";\n\nexport function CellComponent({\n ariaAttributes"
},
{
"path": "integrations/next/app/grid/page.tsx",
"chars": 762,
"preview": "import { Grid } from \"react-window\";\nimport {\n AnimationFrameRowCellCounter,\n EnvironmentMarker,\n LayoutShiftDetecter"
},
{
"path": "integrations/next/app/layout.tsx",
"chars": 761,
"preview": "/* eslint-disable react-refresh/only-export-components */\n\nimport type { Metadata } from \"next\";\nimport { Geist, Geist_M"
},
{
"path": "integrations/next/app/list/components/RowComponent.tsx",
"chars": 296,
"preview": "\"use client\";\n\nimport { type RowComponentProps } from \"react-window\";\n\nexport function RowComponent({\n ariaAttributes,\n"
},
{
"path": "integrations/next/app/list/page.tsx",
"chars": 669,
"preview": "import { List } from \"react-window\";\nimport {\n AnimationFrameRowCellCounter,\n EnvironmentMarker,\n LayoutShiftDetecter"
},
{
"path": "integrations/next/app/list-dynamic/components/List.tsx",
"chars": 460,
"preview": "\"use client\";\n\nimport { List as ListExternal, useDynamicRowHeight } from \"react-window\";\nimport { RowComponent } from \"."
},
{
"path": "integrations/next/app/list-dynamic/components/RowComponent.tsx",
"chars": 296,
"preview": "\"use client\";\n\nimport { type RowComponentProps } from \"react-window\";\n\nexport function RowComponent({\n ariaAttributes,\n"
},
{
"path": "integrations/next/app/list-dynamic/page.tsx",
"chars": 422,
"preview": "import {\n AnimationFrameRowCellCounter,\n EnvironmentMarker,\n LayoutShiftDetecter\n} from \"../../../tests\";\nimport { Li"
},
{
"path": "integrations/next/app/page.tsx",
"chars": 286,
"preview": "import Link from \"next/link\";\n\nexport default async function Home() {\n return (\n <div className=\"p-2 flex flex-col g"
},
{
"path": "integrations/next/app/tailwind.css",
"chars": 512,
"preview": "@source \"../../tests\";\n\n@import \"tailwindcss\";\n\n:root {\n --background: #ffffff;\n --foreground: #171717;\n}\n\n@theme inli"
},
{
"path": "integrations/next/eslint.config.mjs",
"chars": 463,
"preview": "import { defineConfig, globalIgnores } from \"eslint/config\";\nimport nextVitals from \"eslint-config-next/core-web-vitals\""
},
{
"path": "integrations/next/next-env.d.ts",
"chars": 251,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/dev/types/routes.d.ts\";"
},
{
"path": "integrations/next/next.config.ts",
"chars": 133,
"preview": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n /* config options here */\n};\n\nexport default"
},
{
"path": "integrations/next/package.json",
"chars": 576,
"preview": "{\n \"name\": \"next\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev --port 3010\",\n \"buil"
},
{
"path": "integrations/next/postcss.config.mjs",
"chars": 92,
"preview": "const config = {\n plugins: {\n \"@tailwindcss/postcss\": {}\n }\n};\n\nexport default config;\n"
},
{
"path": "integrations/next/tsconfig.json",
"chars": 676,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2017\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n "
},
{
"path": "integrations/tests/package.json",
"chars": 716,
"preview": "{\n \"name\": \"tests\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"main\": \"src/index.ts\",\n \"scripts\":"
},
{
"path": "integrations/tests/playwright.config.ts",
"chars": 410,
"preview": "import { defineConfig, devices } from \"@playwright/test\";\n\nexport default defineConfig({\n projects: [\n {\n name:"
},
{
"path": "integrations/tests/src/components/AnimationFrameRowCellCounter.tsx",
"chars": 763,
"preview": "\"use client\";\n\nimport { useLayoutEffect, useRef } from \"react\";\n\nexport function AnimationFrameRowCellCounter() {\n cons"
},
{
"path": "integrations/tests/src/components/DebugData.tsx",
"chars": 522,
"preview": "import { cn } from \"react-lib-tools\";\n\nexport function DebugData({ data }: { data: object }) {\n return (\n <pre\n "
},
{
"path": "integrations/tests/src/components/Decoder.tsx",
"chars": 768,
"preview": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { Box } from \"react-lib-tools\";\nimport {} from \"react-window\";\nim"
},
{
"path": "integrations/tests/src/components/EnvironmentMarker.tsx",
"chars": 373,
"preview": "export function EnvironmentMarker({ children }: { children: string }) {\n const comment =\n typeof window === \"undefin"
},
{
"path": "integrations/tests/src/components/LayoutShiftDetecter.tsx",
"chars": 1224,
"preview": "\"use client\";\n\nimport { useEffect, useInsertionEffect, useState } from \"react\";\n\ntype PerformanceEntry = {\n hadRecentIn"
},
{
"path": "integrations/tests/src/components/RowComponent.tsx",
"chars": 317,
"preview": "import { type RowComponentProps } from \"react-window\";\n\nexport type RowComponentData = {\n data: string[];\n};\n\nexport fu"
},
{
"path": "integrations/tests/src/index.ts",
"chars": 278,
"preview": "export { Decoder } from \"./components/Decoder\";\nexport { AnimationFrameRowCellCounter } from \"./components/AnimationFram"
},
{
"path": "integrations/tests/src/utils/serializer/decode.ts",
"chars": 1686,
"preview": "import { createElement, type ReactElement } from \"react\";\nimport { List, type ListProps, type RowComponentProps } from \""
},
{
"path": "integrations/tests/src/utils/serializer/encode.ts",
"chars": 2075,
"preview": "import { type ReactElement } from \"react\";\nimport { List, type ListProps, type RowComponentProps } from \"react-window\";\n"
},
{
"path": "integrations/tests/src/utils/serializer/types.ts",
"chars": 620,
"preview": "import type { ListProps, RowComponentProps } from \"react-window\";\nimport type { RowComponentData } from \"../../component"
},
{
"path": "integrations/tests/tests/layout-shift.spec.tsx",
"chars": 2422,
"preview": "import { expect, test, type Page } from \"@playwright/test\";\n\n// High level tests; more nuanced scenarios are covered by "
},
{
"path": "integrations/vike/README.md",
"chars": 1757,
"preview": "Generated with [vike.dev/new](https://vike.dev/new) ([version 531](https://www.npmjs.com/package/create-vike/v/0.0.531))"
},
{
"path": "integrations/vike/package.json",
"chars": 581,
"preview": "{\n \"scripts\": {\n \"dev\": \"vike dev --port 3011\",\n \"build\": \"vike build\",\n \"preview\": \"vike build && vike previe"
},
{
"path": "integrations/vike/pages/+Head.tsx",
"chars": 68,
"preview": "// https://vike.dev/Head\n\nexport function Head() {\n return null;\n}\n"
},
{
"path": "integrations/vike/pages/+Layout.tsx",
"chars": 149,
"preview": "import \"./Layout.css\";\n\nimport \"./tailwind.css\";\n\nexport default function Layout({ children }: { children: React.ReactNo"
},
{
"path": "integrations/vike/pages/+config.ts",
"chars": 400,
"preview": "import type { Config } from \"vike/types\";\nimport vikeReact from \"vike-react/config\";\n\n// Default config (can be overridd"
},
{
"path": "integrations/vike/pages/Layout.css",
"chars": 404,
"preview": "/* Links */\na {\n text-decoration: none;\n}\n#sidebar a {\n padding: 2px 10px;\n margin-left: -10px;\n}\n#sidebar a.is-activ"
},
{
"path": "integrations/vike/pages/_error/+Page.tsx",
"chars": 365,
"preview": "import { usePageContext } from \"vike-react/usePageContext\";\n\nexport default function Page() {\n const { is404 } = usePag"
},
{
"path": "integrations/vike/pages/decoder/+Page.tsx",
"chars": 472,
"preview": "import { useState } from \"react\";\nimport { usePageContext } from \"vike-react/usePageContext\";\nimport { Decoder } from \"."
},
{
"path": "integrations/vike/pages/decoder/+route.ts",
"chars": 20,
"preview": "export default \"*\";\n"
},
{
"path": "integrations/vike/pages/grid/+Page.tsx",
"chars": 742,
"preview": "import { Grid } from \"react-window\";\nimport {\n AnimationFrameRowCellCounter,\n EnvironmentMarker,\n LayoutShiftDetecter"
},
{
"path": "integrations/vike/pages/grid/CellComponent.tsx",
"chars": 321,
"preview": "import { type CellComponentProps } from \"react-window\";\n\nexport function CellComponent({\n ariaAttributes,\n columnIndex"
},
{
"path": "integrations/vike/pages/index/+Page.tsx",
"chars": 231,
"preview": "export default function Page() {\n return (\n <div className=\"p-2 flex flex-col gap-2\">\n <a href=\"/list\">List</a>"
},
{
"path": "integrations/vike/pages/list/+Page.tsx",
"chars": 649,
"preview": "import { List } from \"react-window\";\nimport {\n AnimationFrameRowCellCounter,\n EnvironmentMarker,\n LayoutShiftDetecter"
},
{
"path": "integrations/vike/pages/list/RowComponent.tsx",
"chars": 281,
"preview": "import { type RowComponentProps } from \"react-window\";\n\nexport function RowComponent({\n ariaAttributes,\n index,\n styl"
},
{
"path": "integrations/vike/pages/list-dynamic/+Page.tsx",
"chars": 751,
"preview": "import { List, useDynamicRowHeight } from \"react-window\";\nimport {\n AnimationFrameRowCellCounter,\n EnvironmentMarker,\n"
},
{
"path": "integrations/vike/pages/list-dynamic/RowComponent.tsx",
"chars": 281,
"preview": "import { type RowComponentProps } from \"react-window\";\n\nexport function RowComponent({\n ariaAttributes,\n index,\n styl"
},
{
"path": "integrations/vike/pages/tailwind.css",
"chars": 329,
"preview": "@source \"../../tests\";\n\n@import \"tailwindcss\";\n\n@layer base {\n h1 {\n @apply mb-4 text-4xl font-bold tracking-tight t"
},
{
"path": "integrations/vike/tsconfig.json",
"chars": 406,
"preview": "{\n \"compilerOptions\": {\n \"strict\": true,\n \"target\": \"ES2022\",\n \"module\": \"ES2022\",\n \"moduleResolution\": \"Bu"
},
{
"path": "integrations/vike/vite.config.ts",
"chars": 235,
"preview": "import tailwindcss from \"@tailwindcss/vite\";\nimport react from \"@vitejs/plugin-react\";\nimport vike from \"vike/plugin\";\ni"
},
{
"path": "integrations/vite/README.md",
"chars": 1939,
"preview": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLin"
},
{
"path": "integrations/vite/eslint.config.js",
"chars": 1152,
"preview": "import js from \"@eslint/js\";\nimport globals from \"globals\";\nimport reactHooks from \"eslint-plugin-react-hooks\";\nimport r"
},
{
"path": "integrations/vite/index.html",
"chars": 359,
"preview": "<!doctype html>\n<html class=\"dark\" lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"w"
},
{
"path": "integrations/vite/package.json",
"chars": 789,
"preview": "{\n \"name\": \"vite\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite --port "
},
{
"path": "integrations/vite/src/main.tsx",
"chars": 739,
"preview": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { BrowserRouter, Route, Routes"
},
{
"path": "integrations/vite/src/routes/Decoder.tsx",
"chars": 295,
"preview": "import { useParams, useSearchParams } from \"react-router\";\nimport { Decoder } from \"../../../tests/src\";\n\nexport functio"
},
{
"path": "integrations/vite/src/routes/Grid.tsx",
"chars": 973,
"preview": "import { Grid, type CellComponentProps } from \"react-window\";\nimport {\n AnimationFrameRowCellCounter,\n EnvironmentMark"
},
{
"path": "integrations/vite/src/routes/Home.tsx",
"chars": 213,
"preview": "import { Link } from \"react-router\";\n\nexport function HomeRoute() {\n return (\n <div className=\"p-2 flex flex-col gap"
},
{
"path": "integrations/vite/src/routes/List.tsx",
"chars": 842,
"preview": "import { List, type RowComponentProps } from \"react-window\";\nimport {\n AnimationFrameRowCellCounter,\n EnvironmentMarke"
},
{
"path": "integrations/vite/src/tailwind.css",
"chars": 357,
"preview": "@source \"../../tests\";\n\n@import \"tailwindcss\";\n\n@layer base {\n h1 {\n @apply mb-4 text-4xl font-bold tracking-tight t"
},
{
"path": "integrations/vite/src/vite-env.d.ts",
"chars": 38,
"preview": "/// <reference types=\"vite/client\" />\n"
},
{
"path": "integrations/vite/tsconfig.json",
"chars": 588,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"useDefineForClassFields\": true,\n \"lib\": [\"ES2020\", \"DOM\", \"DOM."
},
{
"path": "integrations/vite/vite.config.ts",
"chars": 255,
"preview": "import tailwindcss from \"@tailwindcss/vite\";\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-reac"
},
{
"path": "lib/components/grid/Grid.test.tsx",
"chars": 17390,
"preview": "import { render, screen } from \"@testing-library/react\";\nimport { createRef, useLayoutEffect } from \"react\";\nimport { be"
},
{
"path": "lib/components/grid/Grid.tsx",
"chars": 7870,
"preview": "\"use client\";\n\nimport {\n createElement,\n memo,\n useEffect,\n useImperativeHandle,\n useMemo,\n useState,\n type React"
},
{
"path": "lib/components/grid/types.ts",
"chars": 9493,
"preview": "import type {\n ComponentProps,\n CSSProperties,\n HTMLAttributes,\n ReactElement,\n ReactNode,\n Ref\n} from \"react\";\nim"
},
{
"path": "lib/components/grid/useGridCallbackRef.ts",
"chars": 351,
"preview": "import { useState } from \"react\";\nimport type { GridImperativeAPI } from \"./types\";\n\n/**\n * Convenience hook to return a"
},
{
"path": "lib/components/grid/useGridRef.ts",
"chars": 236,
"preview": "import { useRef } from \"react\";\nimport type { GridImperativeAPI } from \"./types\";\n\n/**\n * Convenience hook to return a p"
},
{
"path": "lib/components/list/List.test.tsx",
"chars": 22882,
"preview": "import { act, render, screen } from \"@testing-library/react\";\nimport { createRef, useLayoutEffect } from \"react\";\nimport"
},
{
"path": "lib/components/list/List.tsx",
"chars": 5526,
"preview": "\"use client\";\n\nimport {\n createElement,\n memo,\n useEffect,\n useImperativeHandle,\n useMemo,\n useState,\n type React"
},
{
"path": "lib/components/list/isDynamicRowHeight.ts",
"chars": 292,
"preview": "import type { DynamicRowHeight } from \"./types\";\n\nexport function isDynamicRowHeight(value: unknown): value is DynamicRo"
},
{
"path": "lib/components/list/types.ts",
"chars": 6012,
"preview": "import type {\n ComponentProps,\n CSSProperties,\n HTMLAttributes,\n ReactElement,\n ReactNode,\n Ref\n} from \"react\";\nim"
},
{
"path": "lib/components/list/useDynamicRowHeight.test.ts",
"chars": 5482,
"preview": "import { act, renderHook } from \"@testing-library/react\";\nimport { describe, expect, test } from \"vitest\";\nimport { useD"
},
{
"path": "lib/components/list/useDynamicRowHeight.ts",
"chars": 3531,
"preview": "import { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useStableCallback } from \"../../hooks/useSta"
},
{
"path": "lib/components/list/useListCallbackRef.ts",
"chars": 351,
"preview": "import { useState } from \"react\";\nimport type { ListImperativeAPI } from \"./types\";\n\n/**\n * Convenience hook to return a"
},
{
"path": "lib/components/list/useListRef.ts",
"chars": 236,
"preview": "import { useRef } from \"react\";\nimport type { ListImperativeAPI } from \"./types\";\n\n/**\n * Convenience hook to return a p"
},
{
"path": "lib/core/createCachedBounds.test.ts",
"chars": 1721,
"preview": "import { describe, expect, test, vi } from \"vitest\";\nimport { createCachedBounds } from \"./createCachedBounds\";\n\ndescrib"
},
{
"path": "lib/core/createCachedBounds.ts",
"chars": 1655,
"preview": "import { assert } from \"../utils/assert\";\nimport type { Bounds, CachedBounds, SizeFunction } from \"./types\";\n\nexport fun"
},
{
"path": "lib/core/getEstimatedSize.test.ts",
"chars": 2269,
"preview": "import { describe, expect, test } from \"vitest\";\nimport { getEstimatedSize } from \"./getEstimatedSize\";\nimport { createC"
},
{
"path": "lib/core/getEstimatedSize.ts",
"chars": 737,
"preview": "import type { CachedBounds, SizeFunction } from \"./types\";\nimport { assert } from \"../utils/assert\";\n\nexport function ge"
},
{
"path": "lib/core/getOffsetForIndex.test.ts",
"chars": 3101,
"preview": "import { beforeEach, describe, expect, test } from \"vitest\";\nimport { EMPTY_OBJECT } from \"../../src/constants\";\nimport "
},
{
"path": "lib/core/getOffsetForIndex.ts",
"chars": 2152,
"preview": "import type { Align } from \"../types\";\nimport { getEstimatedSize } from \"./getEstimatedSize\";\nimport type { CachedBounds"
},
{
"path": "lib/core/getStartStopIndices.test.ts",
"chars": 4706,
"preview": "import { describe, expect, test } from \"vitest\";\nimport { createCachedBounds } from \"./createCachedBounds\";\nimport { get"
},
{
"path": "lib/core/getStartStopIndices.ts",
"chars": 1553,
"preview": "import type { CachedBounds } from \"./types\";\n\nexport function getStartStopIndices({\n cachedBounds,\n containerScrollOff"
},
{
"path": "lib/core/types.ts",
"chars": 338,
"preview": "export type Bounds = {\n size: number;\n scrollOffset: number;\n};\n\nexport type CachedBounds = {\n get(index: number): Bo"
},
{
"path": "lib/core/useCachedBounds.test.ts",
"chars": 1093,
"preview": "import { renderHook } from \"@testing-library/react\";\nimport { describe, expect, test } from \"vitest\";\nimport { EMPTY_OBJ"
},
{
"path": "lib/core/useCachedBounds.ts",
"chars": 516,
"preview": "import { useMemo } from \"react\";\nimport { createCachedBounds } from \"./createCachedBounds\";\nimport type { CachedBounds, "
},
{
"path": "lib/core/useIsRtl.ts",
"chars": 422,
"preview": "import { useLayoutEffect, useState, type HTMLAttributes } from \"react\";\nimport { isRtl } from \"../utils/isRtl\";\n\nexport "
},
{
"path": "lib/core/useItemSize.ts",
"chars": 839,
"preview": "import { assert } from \"../utils/assert\";\nimport type { SizeFunction } from \"./types\";\n\nexport function useItemSize<Prop"
},
{
"path": "lib/core/useVirtualizer.test.ts",
"chars": 5408,
"preview": "import { renderHook } from \"@testing-library/react\";\nimport { beforeEach, describe, expect, test } from \"vitest\";\nimport"
},
{
"path": "lib/core/useVirtualizer.ts",
"chars": 6874,
"preview": "import {\n useCallback,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties\n} from \"react\";\nimport { useIsomor"
},
{
"path": "lib/hooks/useIsomorphicLayoutEffect.ts",
"chars": 157,
"preview": "import { useEffect, useLayoutEffect } from \"react\";\n\nexport const useIsomorphicLayoutEffect =\n typeof window !== \"undef"
},
{
"path": "lib/hooks/useMemoizedObject.test.ts",
"chars": 1065,
"preview": "import { renderHook } from \"@testing-library/react\";\nimport { describe, expect, test } from \"vitest\";\nimport { useMemoiz"
},
{
"path": "lib/hooks/useMemoizedObject.ts",
"chars": 274,
"preview": "import { useMemo } from \"react\";\n\nexport function useMemoizedObject<Type extends object>(\n unstableObject: Type\n): Type"
},
{
"path": "lib/hooks/useResizeObserver.test.ts",
"chars": 4016,
"preview": "import { act, renderHook } from \"@testing-library/react\";\nimport { beforeEach, describe, expect, test } from \"vitest\";\ni"
},
{
"path": "lib/hooks/useResizeObserver.ts",
"chars": 2168,
"preview": "import { useMemo, useState, type CSSProperties } from \"react\";\nimport { parseNumericStyleValue } from \"../utils/parseNum"
},
{
"path": "lib/hooks/useStableCallback.test.tsx",
"chars": 815,
"preview": "import { fireEvent, render, renderHook, screen } from \"@testing-library/react\";\nimport { describe, expect, test, vi } fr"
},
{
"path": "lib/hooks/useStableCallback.ts",
"chars": 542,
"preview": "import { useCallback, useRef } from \"react\";\nimport { useIsomorphicLayoutEffect } from \"./useIsomorphicLayoutEffect\";\n\n/"
},
{
"path": "lib/index.ts",
"chars": 788,
"preview": "export { Grid } from \"./components/grid/Grid\";\nexport {\n type CellComponentProps,\n type GridImperativeAPI,\n type Grid"
},
{
"path": "lib/types.ts",
"chars": 155,
"preview": "import type { JSX } from \"react\";\n\nexport type Align = \"auto\" | \"center\" | \"end\" | \"smart\" | \"start\";\n\nexport type TagNa"
},
{
"path": "lib/utils/adjustScrollOffsetForRtl.ts",
"chars": 1070,
"preview": "import type { Direction } from \"../core/types\";\nimport { getRTLOffsetType } from \"./getRTLOffsetType\";\n\nexport function "
},
{
"path": "lib/utils/areArraysEqual.ts",
"chars": 256,
"preview": "export function areArraysEqual(a: unknown[], b: unknown[]) {\n if (a.length !== b.length) {\n return false;\n }\n\n for"
},
{
"path": "lib/utils/arePropsEqual.ts",
"chars": 814,
"preview": "import type { CSSProperties } from \"react\";\nimport { shallowCompare } from \"./shallowCompare\";\n\n// Custom comparison fun"
},
{
"path": "lib/utils/assert.ts",
"chars": 212,
"preview": "export function assert(\n expectedCondition: unknown,\n message: string = \"Assertion error\"\n): asserts expectedCondition"
},
{
"path": "lib/utils/colors/getContrastColor.ts",
"chars": 987,
"preview": "export function getContrastColor(hex: string) {\n switch (hex.length) {\n case 3: {\n hex =\n hex.charAt(0) "
},
{
"path": "lib/utils/colors/stringToColor.ts",
"chars": 331,
"preview": "export function stringToColor(string: string) {\n let hash = 0;\n string.split(\"\").forEach((char) => {\n hash = char.c"
},
{
"path": "lib/utils/debug.ts",
"chars": 406,
"preview": "import { getContrastColor } from \"./colors/getContrastColor\";\nimport { stringToColor } from \"./colors/stringToColor\";\n\ne"
},
{
"path": "lib/utils/getRTLOffsetType.ts",
"chars": 1658,
"preview": "export type RTLOffsetType =\n | \"negative\"\n | \"positive-descending\"\n | \"positive-ascending\";\n\nlet cachedRTLResult: RTL"
},
{
"path": "lib/utils/getScrollbarSize.ts",
"chars": 513,
"preview": "let size: number = -1;\n\nexport function getScrollbarSize(recalculate: boolean = false): number {\n if (size === -1 || re"
},
{
"path": "lib/utils/isRtl.ts",
"chars": 279,
"preview": "export function isRtl(element: HTMLElement) {\n let currentElement: HTMLElement | null = element;\n while (currentElemen"
},
{
"path": "lib/utils/parseNumericStyleValue.test.ts",
"chars": 971,
"preview": "import { describe, expect, test } from \"vitest\";\nimport { parseNumericStyleValue } from \"./parseNumericStyleValue\";\n\ndes"
},
{
"path": "lib/utils/parseNumericStyleValue.ts",
"chars": 392,
"preview": "import type { CSSProperties } from \"react\";\n\nexport function parseNumericStyleValue(\n value: CSSProperties[\"height\"]\n):"
},
{
"path": "lib/utils/shallowCompare.test.ts",
"chars": 719,
"preview": "import { describe, expect, test } from \"vitest\";\nimport { shallowCompare } from \"./shallowCompare\";\n\ndescribe(\"shallowCo"
},
{
"path": "lib/utils/shallowCompare.ts",
"chars": 467,
"preview": "import { assert } from \"./assert\";\n\nexport function shallowCompare<Type extends object>(\n a: Type | undefined,\n b: Typ"
},
{
"path": "lib/utils/test/mockResizeObserver.ts",
"chars": 3536,
"preview": "import EventEmitter from \"node:events\";\n\ntype GetDOMRect = (element: HTMLElement) => DOMRectReadOnly | undefined | void;"
},
{
"path": "lib/utils/test/mockScrollTo.ts",
"chars": 797,
"preview": "import { vi } from \"vitest\";\n\nexport function mockScrollTo() {\n const originalScrollTo = HTMLElement.prototype.scrollTo"
},
{
"path": "package.json",
"chars": 3885,
"preview": "{\n \"name\": \"react-window\",\n \"version\": \"2.2.7\",\n \"type\": \"module\",\n \"author\": \"Brian Vaughn <brian.david.vaughn@gmai"
},
{
"path": "pnpm-workspace.yaml",
"chars": 49,
"preview": "packages:\n - integrations/*\n - lib/*\n - src/*\n"
},
{
"path": "postcss.config.js",
"chars": 151,
"preview": "import postcss from \"postcss\";\nimport postcssOKLabFunction from \"@csstools/postcss-oklab-function\";\n\nexport default post"
},
{
"path": "prettier.config.js",
"chars": 125,
"preview": "export default {\n overrides: [\n {\n files: \"*.ts, *.tsx\"\n }\n ],\n singleQuote: false,\n trailingComma: \"none"
},
{
"path": "public/data/addresses.json",
"chars": 374345,
"preview": "[\n {\n \"city\": \"Yuma\",\n \"state\": \"Arizona\",\n \"zip\": \"85364\"\n },\n {\n \"city\": \"Dorchester Center\",\n \"stat"
},
{
"path": "public/data/contacts.json",
"chars": 106540,
"preview": "[\n {\n \"first_name\": \"Vladamir\",\n \"last_name\": \"Adelsberg\",\n \"email\": \"vadelsberg0@nhs.uk\",\n \"gender\": \"Male"
},
{
"path": "public/data/lorem.json",
"chars": 181978,
"preview": "[\n \"Magna fugiat esse commodo sunt mollit est sint mollit ea amet pariatur exercitation cillum.\",\n \"Reprehenderit dolo"
},
{
"path": "public/data/names.json",
"chars": 11754,
"preview": "[\n \"Aaden\",\n \"Aarav\",\n \"Aaron\",\n \"Abdiel\",\n \"Abdullah\",\n \"Abel\",\n \"Abraham\",\n \"Abram\",\n \"Ace\",\n \"Achilles\",\n "
},
{
"path": "public/generated/docs/Grid.json",
"chars": 23888,
"preview": "{\n \"description\": [\n {\n \"content\": \"<p>Renders data with many rows and columns.</p>\\n\"\n },\n {\n \"cont"
},
{
"path": "public/generated/docs/GridImperativeAPI.json",
"chars": 10927,
"preview": "{\n \"description\": [\n {\n \"content\": \"<p>Ref used to interact with this component's imperative API.\\nThis API has"
},
{
"path": "public/generated/docs/List.json",
"chars": 18034,
"preview": "{\n \"description\": [\n {\n \"content\": \"<p>Renders data with many rows.</p>\\n\"\n }\n ],\n \"filePath\": \"lib/compon"
},
{
"path": "public/generated/docs/ListImperativeAPI.json",
"chars": 3369,
"preview": "{\n \"description\": [\n {\n \"content\": \"<p>Imperative List API.</p>\\n\"\n },\n {\n \"content\": \"<p>The <code>"
},
{
"path": "public/generated/examples/BasicRow.json",
"chars": 2673,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/CellComponent.json",
"chars": 4798,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/CellComponentAriaRoles.json",
"chars": 3214,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/FixedHeightList.json",
"chars": 3444,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/FixedHeightRowComponent.json",
"chars": 4780,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/FlexboxLayout.json",
"chars": 14441,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/Grid.json",
"chars": 4017,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/GridAriaRoles.json",
"chars": 2833,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-punctuation\\\"><</span><span class=\\\"tok-typeName\\\">div</span><span class=\\\"\\\"> <"
},
{
"path": "public/generated/examples/HorizontalList.json",
"chars": 4123,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/HorizontalListCellRenderer.json",
"chars": 4600,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/ImageRow.json",
"chars": 4852,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/Images.json",
"chars": 4577,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/ListAriaRoles.json",
"chars": 2801,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-punctuation\\\"><</span><span class=\\\"tok-typeName\\\">div</span><span class=\\\"\\\"> <"
},
{
"path": "public/generated/examples/ListDynamicRowHeights.json",
"chars": 6108,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/ListRowDynamicRowHeights.json",
"chars": 7177,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/ListVariableRowHeights.json",
"chars": 8467,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">type</span><span class=\\\"\\\"> </span><span class=\\\"tok-typeName\\\">Item</spa"
},
{
"path": "public/generated/examples/ListWithStickyRows.json",
"chars": 4096,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/RefComposition.json",
"chars": 5964,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">const</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">[</sp"
},
{
"path": "public/generated/examples/RowComponentAriaRoles.json",
"chars": 3815,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/RtlGrid.json",
"chars": 4208,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/ScrollingIndicator.json",
"chars": 5074,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">const</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">[</sp"
},
{
"path": "public/generated/examples/TableAriaAttributes.json",
"chars": 5867,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-punctuation\\\"><</span><span class=\\\"tok-typeName\\\">div</span><span class=\\\"\\\"> <"
},
{
"path": "public/generated/examples/TableAriaOverrideProps.json",
"chars": 11276,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/columnWidth.json",
"chars": 4622,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">function</span><span class=\\\"\\\"> </span><span class=\\\"tok-variableName tok"
},
{
"path": "public/generated/examples/gridRefClickEventHandler.json",
"chars": 3033,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">const</span><span class=\\\"\\\"> </span><span class=\\\"tok-variableName tok-de"
},
{
"path": "public/generated/examples/listRefClickEventHandler.json",
"chars": 2405,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">const</span><span class=\\\"\\\"> </span><span class=\\\"tok-variableName tok-de"
},
{
"path": "public/generated/examples/rowHeight.json",
"chars": 1719,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">function</span><span class=\\\"\\\"> </span><span class=\\\"tok-variableName tok"
},
{
"path": "public/generated/examples/shared.json",
"chars": 4355,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-keyword\\\">type</sp"
},
{
"path": "public/generated/examples/useGridCallbackRef.json",
"chars": 2864,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/useGridRef.json",
"chars": 1825,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">function</span><span class=\\\"\\\"> </span><span class=\\\"tok-variableName tok"
},
{
"path": "public/generated/examples/useGridRefImport.json",
"chars": 467,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/useListCallbackRef.json",
"chars": 2864,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/examples/useListRef.json",
"chars": 1825,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">function</span><span class=\\\"\\\"> </span><span class=\\\"tok-variableName tok"
},
{
"path": "public/generated/examples/useListRefImport.json",
"chars": 467,
"preview": "{\n \"html\": \"<div><span class=\\\"tok-keyword\\\">import</span><span class=\\\"\\\"> </span><span class=\\\"tok-punctuation\\\">{</s"
},
{
"path": "public/generated/search-index.json",
"chars": 38305,
"preview": "{\n \"keys\": [\n {\n \"path\": [\n \"title\"\n ],\n \"id\": \"title\",\n \"weight\": 1,\n \"src\": \"title"
},
{
"path": "public/generated/search-records.json",
"chars": 34572,
"preview": "[\n {\n \"path\": \"/\",\n \"text\": \"react-window is a component library that helps render large lists of data quickly an"
},
{
"path": "public/robots.txt",
"chars": 22,
"preview": "User-agent: *\nAllow: /"
},
{
"path": "scripts/compile-docs.ts",
"chars": 210,
"preview": "import { compileDocs } from \"react-lib-tools/scripts/compile-docs.ts\";\n\nawait compileDocs({\n componentNames: [\"grid/Gri"
},
{
"path": "scripts/compile-examples.ts",
"chars": 105,
"preview": "import { compileExamples } from \"react-lib-tools/scripts/compile-examples.ts\";\n\nawait compileExamples();\n"
},
{
"path": "scripts/compile-search-index.ts",
"chars": 199,
"preview": "import { compileSearchIndex } from \"react-lib-tools/scripts/compile-search-index.ts\";\n\nawait compileSearchIndex({\n chro"
},
{
"path": "scripts/compress-og-image.ts",
"chars": 106,
"preview": "import { compressOgImage } from \"react-lib-tools/scripts/compress-og-image.ts\";\n\nawait compressOgImage();\n"
},
{
"path": "src/App.tsx",
"chars": 5353,
"preview": "import {\n AppRoot,\n Code,\n ExternalLink,\n NavSection,\n type CommonQuestion\n} from \"react-lib-tools\";\nimport { repos"
},
{
"path": "src/components/ContinueLink.tsx",
"chars": 233,
"preview": "import type { Path } from \"../routes\";\nimport { Link } from \"./Link\";\n\nexport function ContinueLink({ title, to }: { tit"
},
{
"path": "src/components/Link.tsx",
"chars": 283,
"preview": "import type { HTMLAttributes } from \"react\";\nimport { Link as ExternalLink } from \"react-lib-tools\";\nimport type { Path "
},
{
"path": "src/components/NavLink.tsx",
"chars": 413,
"preview": "import { type PropsWithChildren } from \"react\";\nimport { NavLink as NavLinkExternal, type DefaultPath } from \"react-lib-"
},
{
"path": "src/constants.ts",
"chars": 113,
"preview": "export const EMPTY_ARRAY: unknown[] = [];\nexport const EMPTY_OBJECT = {};\nexport const NOOP_FUNCTION = () => {};\n"
},
{
"path": "src/hooks/useLocalStorage.ts",
"chars": 1598,
"preview": "import { useLayoutEffect, useRef, useState } from \"react\";\n\nexport default function useLocalStorage<Type>(\n key: string"
},
{
"path": "src/routes/HowDoesItWorkRoute.tsx",
"chars": 4317,
"preview": "import { ChevronRightIcon } from \"@heroicons/react/20/solid\";\nimport {\n Box,\n Callout,\n Code,\n ExternalLink,\n Heade"
},
{
"path": "src/routes/PlatformRequirementsRoute.tsx",
"chars": 1021,
"preview": "import { Box, Callout, ExternalLink, Header } from \"react-lib-tools\";\n\nexport default function PlatformRequirementsRoute"
},
{
"path": "src/routes/ScratchpadRoute.tsx",
"chars": 3033,
"preview": "import { useCallback, useState, type ButtonHTMLAttributes } from \"react\";\nimport {\n List,\n useDynamicRowHeight,\n useL"
},
{
"path": "src/routes/examples/BasicRow.tsx",
"chars": 321,
"preview": "import { type RowComponentProps } from \"react-window\";\n\n/**\n * Example row.\n *\n * @param index Specifies which row you'r"
},
{
"path": "src/routes/examples/RefComposition.tsx",
"chars": 785,
"preview": "import { useCallback, type Ref } from \"react\";\nimport { List, type ListImperativeAPI, type ListProps } from \"react-windo"
},
{
"path": "src/routes/examples/ScrollingIndicator.tsx",
"chars": 635,
"preview": "import { useEffect, useState } from \"react\";\nimport { List, type ListProps } from \"react-window\";\n\ndeclare const rest: O"
}
]
// ... and 62 more files (download for full content)
About this extraction
This page contains the full source code of the bvaughn/react-window GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 262 files (1.2 MB), approximately 368.8k tokens, and a symbol index with 240 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.