Skip to content

Commit

Permalink
Merge pull request #16 from krjakbrjak/VNI-effects-refactoring
Browse files Browse the repository at this point in the history
Refactored state and effects
  • Loading branch information
krjakbrjak authored Dec 24, 2023
2 parents 1b6d5fa + ecb12df commit d6778e5
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 122 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@krjakbrjak/virtualtable",
"version": "1.1.6",
"version": "1.1.7",
"description": "",
"repository": {
"type": "git",
Expand Down
280 changes: 160 additions & 120 deletions src/VirtualTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,34 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';

import { slideItems } from './helpers/collections';
import { slideItems, Page } from './helpers/collections';

import './base.css';

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

/**
* Represent the rectangular.
*/
interface Rect {
x: number;
y: number;
height: number;
width: number;
}

interface State<Type> {
scrollTop: number,
itemHeight: number,
itemCount: number,
items: Array<Type>,
offset: number,
ready: boolean;
scrollTop: number;
itemHeight: number;
itemCount: number;
page: Page<Type>;
offset: number;
selected: number;
hovered: number;
rect: Rect;
}

interface Action<Type> {
Expand All @@ -39,6 +51,31 @@ interface Args<Type> {
style?: Style;
}

function get_initial_state<T>(): State<T> {
return {
ready: false,
scrollTop: 0,
itemHeight: 0,
itemCount: 0,
page: {
items: [],
offset: 0,
},
offset: 0,
selected: -1,
hovered: -1,
rect: {
x: 0,
y: 0,
height: 0,
width: 0,
}
}
}
function calculatePageCount(pageHeight: number, itemHeight: number) {
return 2 * Math.floor(pageHeight / itemHeight);
}

/**
* Reducer function for managing state changes.
*
Expand All @@ -50,13 +87,9 @@ interface Args<Type> {
function reducer<Type>(state: State<Type>, action: Action<Type>): State<Type> {
switch (action.type) {
case 'scroll':
return { ...state, ...action.data };
case 'render':
return { ...state, ...action.data };
case 'loaded':
return { ...state, ...action.data };
case 'click':
return { ...state, ...action.data };
case 'hover':
return { ...state, ...action.data };
default:
Expand All @@ -79,16 +112,6 @@ function reducer<Type>(state: State<Type>, action: Action<Type>): State<Type> {
* @property {DataSource} fetcher A datasource to fetch the data.
*/

/**
* Represent the rectangular.
*/
interface Rect {
x: number;
y: number;
height: number;
width: number;
}

/**
* @description VirtualTable component.
*
Expand All @@ -99,89 +122,9 @@ interface Rect {
*/
export default function VirtualTable<Type>({ height, renderer, fetcher, style }: Args<Type>): JSX.Element {
const ref = useRef(null);
const invisible = useRef(null);
const [collection, setCollection] = useState<LazyPaginatedCollection<Type>>(() => new LazyPaginatedCollection<Type>(1, fetcher));
const [rect, setRect] = useState<Rect>({
x: 0,
y: 0,
height: 0,
width: 0,
});

useEffect(() => {
setCollection(new LazyPaginatedCollection<Type>(collection.pageSize() ? collection.pageSize() : 1, fetcher));
}, [fetcher]);

const [state, dispatch] = useReducer(reducer<Type>, {
scrollTop: 0,
itemHeight: 0,
itemCount: 0,
items: [],
offset: 0,
selected: -1,
hovered: -1,
});

const [currentOffset, setCurrentOffset] = useState(0);

const calculatePageCount = () => 2 * Math.floor(height / state.itemHeight);

useEffect(() => {
const handler = () => {
if (ref && ref.current) {
setRect(ref.current.getBoundingClientRect());
}
};
window.addEventListener('resize', handler);
return function cleanup() {
window.removeEventListener('resize', handler, true);
}
}, []);

useEffect(() => {
if (collection) {
collection.slice(0, collection.pageSize()).then((result) => {
dispatch({
type: 'loaded',
data: {
scrollTop: 0,
itemHeight: 0,
items: [],
offset: 0,
selected: -1,
hovered: -1,
...result,
itemCount: collection.count(),
},
});
});
}
}, [collection]);

useEffect(() => {
if (state.itemHeight) {
const offset = Math.floor(state.scrollTop / state.itemHeight);
const c = calculatePageCount();
if (c !== collection.pageSize()) {
setCurrentOffset(0);
setCollection(new LazyPaginatedCollection<Type>(c, fetcher));
} else {
setCurrentOffset(offset);
collection.slice(offset, collection.pageSize()).then((result) => {
if (currentOffset !== result.offset) {
dispatch({
type: 'loaded',
data: {
...result,
itemCount: collection.count(),
},
});
}
});
}
}
}, [
state,
]);
const [state, dispatch] = useReducer(reducer<Type>, get_initial_state<Type>());

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


// A callback to update the table view in case of resize event.
const handler = () => {
let itemHeight = state.itemHeight;
let rect = state.rect;
if (invisible && invisible.current) {
itemHeight = invisible.current.clientHeight;
}
if (ref && ref.current) {
rect = ref.current.getBoundingClientRect();
}

// Update the size of the widget and the size of the items
dispatch({
type: 'render',
data: {
rect,
itemHeight,
scrollTop: 0,
selected: -1,
hovered: -1,
page: {
items: [],
offset: 0,
}
},
});

// If the item's height is already known, then update the lazy collection
// and re-fetch the items.
if (itemHeight) {
const new_collection = new LazyPaginatedCollection<Type>(calculatePageCount(rect.height, itemHeight), fetcher);
new_collection.slice(0, new_collection.pageSize()).then((result) => {
dispatch({
type: 'loaded',
data: {
page: result,
itemCount: new_collection.count(),
},
});
setCollection(new_collection);
});
}
};

// Effect that updates the lazy collection in case fetcher gets updated
useEffect(() => {
if (ref.current) {
ref.current.scrollTop = state.scrollTop % state.itemHeight;
if (ref.current.children && ref.current.children.length) {
if (ref.current.children[0].clientHeight !== state.itemHeight) {
setRect(ref.current.getBoundingClientRect());
setCollection(new LazyPaginatedCollection<Type>(collection.pageSize() ? collection.pageSize() : 1, fetcher));
}, [fetcher]);

// Effect to fetch the first item (to draw a fake item to get the true size if the item)
// and the total number of items.
useEffect(() => {
collection.slice(0, collection.pageSize()).then((result) => {
dispatch({
type: 'loaded',
data: {
ready: true,
page: result,
itemCount: collection.count(),
},
});
});

window.addEventListener('resize', handler);
return function cleanup() {
window.removeEventListener('resize', handler, true);
}
}, []);

// Effect to run on all state updates.
useEffect(() => {
if (state.ready) {
if (state.itemHeight) {
const offset = Math.floor(state.scrollTop / state.itemHeight);
const c = calculatePageCount(height, state.itemHeight);
if (c === collection.pageSize() && state.offset !== offset) {
// Update the offset first and then start fetching the necessary items.
// This ensures a non-interruptive user experience, where all the
// required data is already available.
dispatch({
type: 'render',
type: 'loaded',
data: {
itemHeight: ref.current.children[0].clientHeight,
offset,
},
});
collection.slice(offset, collection.pageSize()).then((result) => {
if (state.offset !== result.offset) {
dispatch({
type: 'loaded',
data: {
page: result,
itemCount: collection.count(),
},
});
}
});
}
} else {
handler();
}
}
}, [state]);

// Effect to run on each render to make sure that the scrolltop of
// the item container is up-to-date.
useEffect(() => {
if (ref.current) {
ref.current.scrollTop = state.scrollTop % state.itemHeight;
}
});

if (state.items.length === 0) {
return <div />;
}

return (
<Container>
Expand All @@ -233,18 +268,23 @@ export default function VirtualTable<Type>({ height, renderer, fetcher, style }:
height,
}}
>
{generate(currentOffset, slideItems(currentOffset, {
items: state.items,
offset: state.offset,
}))}
{state.ready && state.itemHeight === 0 &&
<div ref={invisible} style={{
'visibility': 'hidden',
position: 'absolute',
pointerEvents: 'none'
}}>
{renderer(state.page.items[0], '')}
</div>}
{state.itemHeight !== 0 && generate(state.offset, slideItems(state.offset, state.page))}
</div>
<div
className='overflow-scroll position-absolute'
style={{
top: rect.y,
left: rect.x,
width: rect.width,
height: rect.height,
top: state.rect.y,
left: state.rect.x,
width: state.rect.width,
height: state.rect.height,
}}
onMouseMove={(e) => {
const index = Math.floor((e.clientY + state.scrollTop - ref.current.getBoundingClientRect().top) / state.itemHeight);
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Represent a page.
* @typedef {Object} Slice
*/
interface Page<Type> {
export interface Page<Type> {
/**
* Page items
*/
Expand Down

0 comments on commit d6778e5

Please sign in to comment.