Skip to content

Commit d6778e5

Browse files
authored
Merge pull request #16 from krjakbrjak/VNI-effects-refactoring
Refactored state and effects
2 parents 1b6d5fa + ecb12df commit d6778e5

File tree

3 files changed

+162
-122
lines changed

3 files changed

+162
-122
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@krjakbrjak/virtualtable",
3-
"version": "1.1.6",
3+
"version": "1.1.7",
44
"description": "",
55
"repository": {
66
"type": "git",

src/VirtualTable.tsx

Lines changed: 160 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,34 @@ import React, {
99
} from 'react';
1010
import PropTypes from 'prop-types';
1111

12-
import { slideItems } from './helpers/collections';
12+
import { slideItems, Page } from './helpers/collections';
1313

1414
import './base.css';
1515

1616
import { LazyPaginatedCollection } from './helpers/LazyPaginatedCollection';
1717
import { Style, DataSource } from './helpers/types';
1818
import { Container, Row, Col } from 'react-bootstrap';
1919

20+
/**
21+
* Represent the rectangular.
22+
*/
23+
interface Rect {
24+
x: number;
25+
y: number;
26+
height: number;
27+
width: number;
28+
}
29+
2030
interface State<Type> {
21-
scrollTop: number,
22-
itemHeight: number,
23-
itemCount: number,
24-
items: Array<Type>,
25-
offset: number,
31+
ready: boolean;
32+
scrollTop: number;
33+
itemHeight: number;
34+
itemCount: number;
35+
page: Page<Type>;
36+
offset: number;
2637
selected: number;
2738
hovered: number;
39+
rect: Rect;
2840
}
2941

3042
interface Action<Type> {
@@ -39,6 +51,31 @@ interface Args<Type> {
3951
style?: Style;
4052
}
4153

54+
function get_initial_state<T>(): State<T> {
55+
return {
56+
ready: false,
57+
scrollTop: 0,
58+
itemHeight: 0,
59+
itemCount: 0,
60+
page: {
61+
items: [],
62+
offset: 0,
63+
},
64+
offset: 0,
65+
selected: -1,
66+
hovered: -1,
67+
rect: {
68+
x: 0,
69+
y: 0,
70+
height: 0,
71+
width: 0,
72+
}
73+
}
74+
}
75+
function calculatePageCount(pageHeight: number, itemHeight: number) {
76+
return 2 * Math.floor(pageHeight / itemHeight);
77+
}
78+
4279
/**
4380
* Reducer function for managing state changes.
4481
*
@@ -50,13 +87,9 @@ interface Args<Type> {
5087
function reducer<Type>(state: State<Type>, action: Action<Type>): State<Type> {
5188
switch (action.type) {
5289
case 'scroll':
53-
return { ...state, ...action.data };
5490
case 'render':
55-
return { ...state, ...action.data };
5691
case 'loaded':
57-
return { ...state, ...action.data };
5892
case 'click':
59-
return { ...state, ...action.data };
6093
case 'hover':
6194
return { ...state, ...action.data };
6295
default:
@@ -79,16 +112,6 @@ function reducer<Type>(state: State<Type>, action: Action<Type>): State<Type> {
79112
* @property {DataSource} fetcher A datasource to fetch the data.
80113
*/
81114

82-
/**
83-
* Represent the rectangular.
84-
*/
85-
interface Rect {
86-
x: number;
87-
y: number;
88-
height: number;
89-
width: number;
90-
}
91-
92115
/**
93116
* @description VirtualTable component.
94117
*
@@ -99,89 +122,9 @@ interface Rect {
99122
*/
100123
export default function VirtualTable<Type>({ height, renderer, fetcher, style }: Args<Type>): JSX.Element {
101124
const ref = useRef(null);
125+
const invisible = useRef(null);
102126
const [collection, setCollection] = useState<LazyPaginatedCollection<Type>>(() => new LazyPaginatedCollection<Type>(1, fetcher));
103-
const [rect, setRect] = useState<Rect>({
104-
x: 0,
105-
y: 0,
106-
height: 0,
107-
width: 0,
108-
});
109-
110-
useEffect(() => {
111-
setCollection(new LazyPaginatedCollection<Type>(collection.pageSize() ? collection.pageSize() : 1, fetcher));
112-
}, [fetcher]);
113-
114-
const [state, dispatch] = useReducer(reducer<Type>, {
115-
scrollTop: 0,
116-
itemHeight: 0,
117-
itemCount: 0,
118-
items: [],
119-
offset: 0,
120-
selected: -1,
121-
hovered: -1,
122-
});
123-
124-
const [currentOffset, setCurrentOffset] = useState(0);
125-
126-
const calculatePageCount = () => 2 * Math.floor(height / state.itemHeight);
127-
128-
useEffect(() => {
129-
const handler = () => {
130-
if (ref && ref.current) {
131-
setRect(ref.current.getBoundingClientRect());
132-
}
133-
};
134-
window.addEventListener('resize', handler);
135-
return function cleanup() {
136-
window.removeEventListener('resize', handler, true);
137-
}
138-
}, []);
139-
140-
useEffect(() => {
141-
if (collection) {
142-
collection.slice(0, collection.pageSize()).then((result) => {
143-
dispatch({
144-
type: 'loaded',
145-
data: {
146-
scrollTop: 0,
147-
itemHeight: 0,
148-
items: [],
149-
offset: 0,
150-
selected: -1,
151-
hovered: -1,
152-
...result,
153-
itemCount: collection.count(),
154-
},
155-
});
156-
});
157-
}
158-
}, [collection]);
159-
160-
useEffect(() => {
161-
if (state.itemHeight) {
162-
const offset = Math.floor(state.scrollTop / state.itemHeight);
163-
const c = calculatePageCount();
164-
if (c !== collection.pageSize()) {
165-
setCurrentOffset(0);
166-
setCollection(new LazyPaginatedCollection<Type>(c, fetcher));
167-
} else {
168-
setCurrentOffset(offset);
169-
collection.slice(offset, collection.pageSize()).then((result) => {
170-
if (currentOffset !== result.offset) {
171-
dispatch({
172-
type: 'loaded',
173-
data: {
174-
...result,
175-
itemCount: collection.count(),
176-
},
177-
});
178-
}
179-
});
180-
}
181-
}
182-
}, [
183-
state,
184-
]);
127+
const [state, dispatch] = useReducer(reducer<Type>, get_initial_state<Type>());
185128

186129
const generate = (offset: number, d: Array<Type>) => {
187130
const ret = [];
@@ -201,26 +144,118 @@ export default function VirtualTable<Type>({ height, renderer, fetcher, style }:
201144
return ret;
202145
};
203146

147+
148+
// A callback to update the table view in case of resize event.
149+
const handler = () => {
150+
let itemHeight = state.itemHeight;
151+
let rect = state.rect;
152+
if (invisible && invisible.current) {
153+
itemHeight = invisible.current.clientHeight;
154+
}
155+
if (ref && ref.current) {
156+
rect = ref.current.getBoundingClientRect();
157+
}
158+
159+
// Update the size of the widget and the size of the items
160+
dispatch({
161+
type: 'render',
162+
data: {
163+
rect,
164+
itemHeight,
165+
scrollTop: 0,
166+
selected: -1,
167+
hovered: -1,
168+
page: {
169+
items: [],
170+
offset: 0,
171+
}
172+
},
173+
});
174+
175+
// If the item's height is already known, then update the lazy collection
176+
// and re-fetch the items.
177+
if (itemHeight) {
178+
const new_collection = new LazyPaginatedCollection<Type>(calculatePageCount(rect.height, itemHeight), fetcher);
179+
new_collection.slice(0, new_collection.pageSize()).then((result) => {
180+
dispatch({
181+
type: 'loaded',
182+
data: {
183+
page: result,
184+
itemCount: new_collection.count(),
185+
},
186+
});
187+
setCollection(new_collection);
188+
});
189+
}
190+
};
191+
192+
// Effect that updates the lazy collection in case fetcher gets updated
204193
useEffect(() => {
205-
if (ref.current) {
206-
ref.current.scrollTop = state.scrollTop % state.itemHeight;
207-
if (ref.current.children && ref.current.children.length) {
208-
if (ref.current.children[0].clientHeight !== state.itemHeight) {
209-
setRect(ref.current.getBoundingClientRect());
194+
setCollection(new LazyPaginatedCollection<Type>(collection.pageSize() ? collection.pageSize() : 1, fetcher));
195+
}, [fetcher]);
196+
197+
// Effect to fetch the first item (to draw a fake item to get the true size if the item)
198+
// and the total number of items.
199+
useEffect(() => {
200+
collection.slice(0, collection.pageSize()).then((result) => {
201+
dispatch({
202+
type: 'loaded',
203+
data: {
204+
ready: true,
205+
page: result,
206+
itemCount: collection.count(),
207+
},
208+
});
209+
});
210+
211+
window.addEventListener('resize', handler);
212+
return function cleanup() {
213+
window.removeEventListener('resize', handler, true);
214+
}
215+
}, []);
216+
217+
// Effect to run on all state updates.
218+
useEffect(() => {
219+
if (state.ready) {
220+
if (state.itemHeight) {
221+
const offset = Math.floor(state.scrollTop / state.itemHeight);
222+
const c = calculatePageCount(height, state.itemHeight);
223+
if (c === collection.pageSize() && state.offset !== offset) {
224+
// Update the offset first and then start fetching the necessary items.
225+
// This ensures a non-interruptive user experience, where all the
226+
// required data is already available.
210227
dispatch({
211-
type: 'render',
228+
type: 'loaded',
212229
data: {
213-
itemHeight: ref.current.children[0].clientHeight,
230+
offset,
214231
},
215232
});
233+
collection.slice(offset, collection.pageSize()).then((result) => {
234+
if (state.offset !== result.offset) {
235+
dispatch({
236+
type: 'loaded',
237+
data: {
238+
page: result,
239+
itemCount: collection.count(),
240+
},
241+
});
242+
}
243+
});
216244
}
245+
} else {
246+
handler();
217247
}
218248
}
249+
}, [state]);
250+
251+
// Effect to run on each render to make sure that the scrolltop of
252+
// the item container is up-to-date.
253+
useEffect(() => {
254+
if (ref.current) {
255+
ref.current.scrollTop = state.scrollTop % state.itemHeight;
256+
}
219257
});
220258

221-
if (state.items.length === 0) {
222-
return <div />;
223-
}
224259

225260
return (
226261
<Container>
@@ -233,18 +268,23 @@ export default function VirtualTable<Type>({ height, renderer, fetcher, style }:
233268
height,
234269
}}
235270
>
236-
{generate(currentOffset, slideItems(currentOffset, {
237-
items: state.items,
238-
offset: state.offset,
239-
}))}
271+
{state.ready && state.itemHeight === 0 &&
272+
<div ref={invisible} style={{
273+
'visibility': 'hidden',
274+
position: 'absolute',
275+
pointerEvents: 'none'
276+
}}>
277+
{renderer(state.page.items[0], '')}
278+
</div>}
279+
{state.itemHeight !== 0 && generate(state.offset, slideItems(state.offset, state.page))}
240280
</div>
241281
<div
242282
className='overflow-scroll position-absolute'
243283
style={{
244-
top: rect.y,
245-
left: rect.x,
246-
width: rect.width,
247-
height: rect.height,
284+
top: state.rect.y,
285+
left: state.rect.x,
286+
width: state.rect.width,
287+
height: state.rect.height,
248288
}}
249289
onMouseMove={(e) => {
250290
const index = Math.floor((e.clientY + state.scrollTop - ref.current.getBoundingClientRect().top) / state.itemHeight);

src/helpers/collections.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Represent a page.
33
* @typedef {Object} Slice
44
*/
5-
interface Page<Type> {
5+
export interface Page<Type> {
66
/**
77
* Page items
88
*/

0 commit comments

Comments
 (0)