Skip to content

Commit ab83bb6

Browse files
authored
Merge pull request #46 from thefrontside/pc/virtualized-infinite-scroll
Virtualized infinite-scroll
2 parents a59dc99 + b2d1610 commit ab83bb6

File tree

20 files changed

+354
-94
lines changed

20 files changed

+354
-94
lines changed

cli/.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"deno.enable": false
3+
}

cli/app/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<link rel="icon" href="favicon.ico" />
1010
</head>
1111
<body>
12-
<div id="main"></div>
12+
<main></main>
1313
<script type="module" src="src/index.tsx"></script>
1414
</body>
1515
</html>
Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
#main {
1+
main {
22
display: grid;
3-
grid-template-rows: [top] 2.125rem [main] 1fr;
3+
grid-template-rows: [top] 2.125rem [main] auto;
44
}
55

66
.app {
77
grid-row: main;
88
margin-top: .5rem;
9-
}
10-
11-
.app section:first-of-type {
12-
display: flex;
13-
justify-content: flex-start;
149
}

cli/app/src/components/Factory/Factory.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ import { StrictMode, Suspense } from "react";
33
import { StyledEngineProvider } from "@mui/material/styles";
44
import { GraphInspector } from "../GraphInspector/GraphInspector";
55
import { Topbar } from "../Topbar/Topbar";
6-
import { createClient, dedupExchange, Exchange, fetchExchange, Provider } from "urql";
6+
import {
7+
createClient,
8+
dedupExchange,
9+
Exchange,
10+
fetchExchange,
11+
Provider,
12+
} from "urql";
713
import { cacheExchange } from "@urql/exchange-graphcache";
8-
import { relayPagination } from '@urql/exchange-graphcache/extras';
14+
import { relayPagination } from "@urql/exchange-graphcache/extras";
915

1016
const client = createClient({
1117
url: "/graphql",
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { useCallback, useEffect, useRef } from "react";
2+
import { useVirtualizer } from "@tanstack/react-virtual";
3+
import type { VertexNode } from "../../../../graphql/types";
4+
import { VirtualRow } from "./VirtualRow";
5+
6+
interface DynamicRowVirtualizerProps {
7+
nodes: VertexNode[];
8+
typename: string;
9+
hasNextPage: boolean;
10+
fetching: boolean;
11+
fetchNextPage: () => void;
12+
update: number;
13+
}
14+
15+
const RowSize = 30;
16+
17+
export function DynamicRowVirtualizer(
18+
{ nodes, typename, hasNextPage, fetching, fetchNextPage, update }:
19+
DynamicRowVirtualizerProps,
20+
): JSX.Element {
21+
const expanderRef = useRef<HTMLDivElement>();
22+
23+
const rowVirtualizer = useVirtualizer({
24+
count: nodes.length,
25+
getScrollElement: () => expanderRef.current,
26+
// we need useCallback to force the update
27+
estimateSize: useCallback(() => nodes[0].fields.length * RowSize, [update]),
28+
enableSmoothScroll: false,
29+
getItemKey: (index) => nodes[index].id,
30+
// nuking this for now. Default does too much
31+
scrollToFn: () => ({}),
32+
});
33+
34+
useEffect(() => {
35+
const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse();
36+
37+
if (!lastItem) {
38+
return;
39+
}
40+
41+
if (
42+
lastItem.index >= nodes.length - 1 &&
43+
hasNextPage &&
44+
!fetching
45+
) {
46+
fetchNextPage();
47+
}
48+
}, [
49+
hasNextPage,
50+
fetchNextPage,
51+
nodes.length,
52+
fetching,
53+
rowVirtualizer.getVirtualItems(),
54+
]);
55+
56+
return (
57+
<div
58+
ref={expanderRef}
59+
style={{
60+
height: `${window.innerHeight - 250}px`,
61+
width: `100%`,
62+
overflow: "auto",
63+
}}
64+
>
65+
<div
66+
style={{
67+
height: rowVirtualizer.getTotalSize(),
68+
width: "100%",
69+
position: "relative",
70+
}}
71+
>
72+
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
73+
const vertexNode = nodes[virtualRow.index];
74+
75+
return (
76+
<VirtualRow
77+
key={`${nodes[virtualRow.index].id}`}
78+
vertexNode={vertexNode}
79+
typename={typename}
80+
virtualRow={virtualRow}
81+
update={update}
82+
/>
83+
);
84+
})}
85+
</div>
86+
</div>
87+
);
88+
}

cli/app/src/components/GraphInspector/GraphInspector.tsx

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,49 @@
11
import "./GraphInspector.css";
22
import type { SyntheticEvent } from "react";
3-
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
3+
import { useCallback, useEffect, useReducer, useState, useRef } from "react";
44
import TreeView from "@mui/lab/TreeView";
5-
import { Node } from "./Node";
65
import { allQuery, node } from "./queries";
76
import { graphReducer } from "./graphReducer";
87
import { VertexNode } from "../../../../graphql/types";
98
import { MinusSquare, PlusSquare } from "./icons";
109
import { StyledTreeItem } from "./StyledTreeItem";
1110
import { fetchGraphQL } from "../../graphql/fetchGraphql";
12-
import { Loader } from "../Loader/Loader";
13-
import { useQuery } from 'urql';
14-
import type { Page } from '../../../../graphql/relay';
11+
import { useQuery } from "urql";
12+
import type { Page } from "../../../../graphql/relay";
13+
import { DynamicRowVirtualizer } from "./DynamicRowVirtualizer";
14+
1515
const emptyGraph = { graph: {} };
1616

1717
const limit = 5;
1818

1919
export function GraphInspector(): JSX.Element {
20-
// TODO: call setAfter when scrolling
21-
const [after] = useState('');
20+
const [after, setAfter] = useState("");
2221
const [typename, setTypename] = useState<string | undefined>();
22+
// TODO: this really needs to go at some point and the rangeExtractor prop of
23+
// useVirtualized seems a better way to go
24+
// https://tanstack.com/virtual/v3/docs/api/virtualizer#rangeextractor
25+
// measureElement might be useful here too
26+
// https://tanstack.com/virtual/v3/docs/api/virtualizer#measureelement
27+
const [update, forceUpdate] = useReducer((x) => x + 1, 0);
2328

2429
const [result] = useQuery<{ all: Page<VertexNode> }, {
2530
typename: string;
2631
first: number;
2732
after: string;
2833
}>({
2934
query: allQuery,
30-
pause: !typename,
35+
pause: !typename && !after,
3136
variables: {
3237
typename,
3338
first: limit,
34-
after
39+
after,
3540
},
3641
});
3742

3843
const [{ graph }, dispatch] = useReducer(graphReducer, emptyGraph);
3944
const expandedNodes = useRef(new Set<string>());
4045

41-
const { data, error } = result;
46+
const { data, error, fetching } = result;
4247

4348
useEffect(() => {
4449
const edges = data?.all?.edges ?? [];
@@ -51,10 +56,10 @@ export function GraphInspector(): JSX.Element {
5156
type: "ALL",
5257
payload: {
5358
typename,
54-
nodes: edges.map(edge => edge.node),
59+
nodes: edges.map((edge) => edge.node),
5560
},
5661
});
57-
}, [data, typename])
62+
}, [data, typename]);
5863

5964
const handleChange = useCallback(
6065
async (_: SyntheticEvent, nodeIds: string[]) => {
@@ -115,11 +120,17 @@ export function GraphInspector(): JSX.Element {
115120
}
116121
}
117122

123+
setTimeout(forceUpdate, 300);
124+
118125
return;
119126
}
120127

128+
setAfter("");
121129
setTypename(nodeId);
122-
}, [],
130+
131+
setTimeout(forceUpdate, 300);
132+
},
133+
[],
123134
);
124135

125136
useEffect(() => {
@@ -146,40 +157,39 @@ export function GraphInspector(): JSX.Element {
146157
}
147158

148159
if (error) {
149-
return <p className="error">Oh no... {error?.message}</p>
160+
return <p className="error">Oh no... {error?.message}</p>;
150161
}
151162

152163
return (
153-
<TreeView
154-
aria-label="graph inspector"
155-
defaultCollapseIcon={<MinusSquare />}
156-
defaultExpandIcon={<PlusSquare />}
157-
onNodeToggle={handleChange}
158-
multiSelect={false}
159-
>
160-
{Object.values(graph).map(({ typename, label, nodes }) => (
161-
<StyledTreeItem
162-
key={typename}
163-
nodeId={typename}
164-
label={<div className="root">{label}</div>}
165-
>
166-
{nodes.length > 0
167-
? nodes.map((vertexNode, i) => (
168-
<StyledTreeItem
169-
key={vertexNode.id}
170-
nodeId={vertexNode.id}
171-
label={
172-
<Node
173-
parentId={`${typename}.nodes.${i}`}
174-
node={vertexNode}
175-
/>
176-
}
177-
/>
178-
)
179-
)
180-
: <Loader />}
181-
</StyledTreeItem>
182-
))}
183-
</TreeView>
164+
<div>
165+
<TreeView
166+
aria-label="graph inspector"
167+
defaultCollapseIcon={<MinusSquare />}
168+
defaultExpandIcon={<PlusSquare />}
169+
onNodeToggle={handleChange}
170+
multiSelect={false}
171+
>
172+
{Object.values(graph).map(({ typename, label, nodes }) => (
173+
<StyledTreeItem
174+
key={typename}
175+
nodeId={typename}
176+
label={<div className="root">{label}</div>}
177+
>
178+
{nodes.length > 0
179+
? (
180+
<DynamicRowVirtualizer
181+
hasNextPage={!!data?.all?.pageInfo?.hasNextPage}
182+
nodes={nodes}
183+
typename={typename}
184+
fetching={fetching}
185+
fetchNextPage={() => setAfter(data.all.pageInfo.endCursor)}
186+
update={update}
187+
/>
188+
)
189+
: <div>loading....</div>}
190+
</StyledTreeItem>
191+
))}
192+
</TreeView>
193+
</div>
184194
);
185195
}

cli/app/src/components/GraphInspector/StyledTreeItem.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import { alpha, styled } from "@mui/material/styles";
22
import TreeItem, { treeItemClasses, TreeItemProps } from "@mui/lab/TreeItem";
3-
import { FC } from 'react';
3+
import { FC } from "react";
44

5-
export const StyledTreeItem: FC<TreeItemProps> = styled((props: TreeItemProps) => (
6-
<TreeItem {...props} />
7-
))(({ theme }) => ({
5+
export const StyledTreeItem: FC<TreeItemProps> = styled((
6+
props: TreeItemProps,
7+
) => <TreeItem {...props} />)(({ theme }) => ({
88
[`& .${treeItemClasses.iconContainer}`]: {
99
"& .close": {
1010
opacity: 0.3,
1111
},
1212
},
1313
[`& .${treeItemClasses.group}`]: {
14-
marginLeft: `15px !important`,
15-
paddingLeft: `18px !important`,
1614
borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`,
1715
},
1816
[`& .${treeItemClasses.selected}`]: {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { StyledTreeItem } from "./StyledTreeItem";
2+
import { Node } from "./Node";
3+
import { VirtualItem } from "@tanstack/react-virtual";
4+
import type { VertexNode } from "../../../../graphql/types";
5+
import { useEffect, useRef } from "react";
6+
7+
interface VirtualRowProps {
8+
virtualRow: VirtualItem<unknown>;
9+
vertexNode: VertexNode;
10+
typename: string;
11+
update: number;
12+
}
13+
14+
export function VirtualRow(
15+
{ virtualRow, vertexNode, typename, update }: VirtualRowProps,
16+
): JSX.Element {
17+
const elementRef = useRef<HTMLDivElement>(null);
18+
19+
useEffect(() => {
20+
virtualRow.measureElement(elementRef.current);
21+
}, [update]);
22+
23+
return (
24+
<div
25+
key={vertexNode.id}
26+
ref={elementRef}
27+
style={{
28+
position: "absolute",
29+
top: 0,
30+
left: 0,
31+
width: "100%",
32+
transform: `translateY(${virtualRow.start}px)`,
33+
}}
34+
>
35+
<StyledTreeItem
36+
key={vertexNode.id}
37+
nodeId={vertexNode.id}
38+
label={
39+
<Node
40+
parentId={`${typename}.nodes.${virtualRow.index}`}
41+
node={vertexNode}
42+
/>
43+
}
44+
/>
45+
</div>
46+
);
47+
}

0 commit comments

Comments
 (0)