From 997d44155379604149a7aea576e71ef75c3920bd Mon Sep 17 00:00:00 2001 From: Vladimir Kruzhkov Date: Sun, 28 Jul 2024 23:33:47 +0300 Subject: [PATCH] hooks now returns itemSlice, infinity loader example --- build_examples/demo.js | 84 +++++++++++++++++++++++++----------------- package.json | 2 +- readme.md | 56 ++++++++++++++++++++++++++++ src/examples/demo.tsx | 13 +++++-- src/index.tsx | 20 +++------- src/small-utils.ts | 11 ++++++ 6 files changed, 134 insertions(+), 52 deletions(-) create mode 100644 src/small-utils.ts diff --git a/build_examples/demo.js b/build_examples/demo.js index 707670a..2352b36 100644 --- a/build_examples/demo.js +++ b/build_examples/demo.js @@ -37,11 +37,11 @@ const fixed_grid_1 = require("../fixed-grid"); const itemsLine = Array.from({ length: 300 }).map((_, i) => `item ${i}`); const itemsGrid = Array.from({ length: 300 }).map((_, iy) => Array.from({ length: 300 }).map((_, ix) => `item ${ix} ${iy}`)); function ListWithHookExample() { - const items = itemsLine; + const [items, setItems] = (0, react_1.useState)([]); const containerRef = (0, react_1.useRef)(undefined); const infoRef = (0, react_1.useRef)(undefined); const itemHeight = 40; - const { renderedItems, updateViewRect } = (0, __1.useVirtualOverflowY)({ + const { renderedItems, updateViewRect, itemSlice } = (0, __1.useVirtualOverflowY)({ containerRef, itemHeight, itemsLengthY: items.length, @@ -57,6 +57,12 @@ function ListWithHookExample() { infoRef.current.innerText = `Visible rect of content:\n\n${JSON.stringify(visibleRect, null, 2)}`; }, 24); }, []); + (0, react_1.useEffect)(() => { + if (itemSlice.topStartIndex + itemSlice.lengthY >= items.length - 4) { + // load more + setItems(prev => [...prev, ...itemsLine]); + } + }, [itemSlice.topStartIndex, itemSlice.lengthY]); return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { ref: infoRef, style: { position: 'fixed', top: 0, right: 0, paddingRight: '40px', width: '200px' } }), (0, jsx_runtime_1.jsx)("div", { style: { overflowY: 'scroll', height: '300px', background: 'lightgreen' }, children: (0, jsx_runtime_1.jsx)("div", { ref: containerRef, style: { position: 'relative', height: `${itemHeight * items.length}px` }, children: renderedItems }) })] })); } function VerticalListExample() { @@ -80,7 +86,7 @@ const rootElement = document.getElementById("demo"); const root = client_1.default.createRoot(rootElement); root.render((0, jsx_runtime_1.jsx)(react_1.default.StrictMode, { children: (0, jsx_runtime_1.jsx)(App, {}) })); -},{"..":4,"../fixed-grid":2,"../fixed-list-y":3,"../utils":5,"react":15,"react-dom/client":9,"react/jsx-runtime":16}],2:[function(require,module,exports){ +},{"..":4,"../fixed-grid":2,"../fixed-list-y":3,"../utils":6,"react":16,"react-dom/client":10,"react/jsx-runtime":17}],2:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VirtualGrid = void 0; @@ -108,7 +114,7 @@ function VirtualGrid(props) { } exports.VirtualGrid = VirtualGrid; -},{".":4,"react":15,"react/jsx-runtime":16}],3:[function(require,module,exports){ +},{".":4,"react":16,"react/jsx-runtime":17}],3:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VirtualListY = void 0; @@ -134,21 +140,12 @@ function VirtualListY(props) { } exports.VirtualListY = VirtualListY; -},{".":4,"react":15,"react/jsx-runtime":16}],4:[function(require,module,exports){ +},{".":4,"react":16,"react/jsx-runtime":17}],4:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useVirtualOverflowGrid = exports.useVirtualOverflowX = exports.useVirtualOverflowY = exports.useCalcVirtualOverflow = exports.virtualOverflowCalcItems = exports.virtualOverflowCalcVisibleRect = void 0; const react_1 = require("react"); -function debounceAnimationFrame(func) { - let frameRequest = 0; - return { - requestFrame: () => { - cancelAnimationFrame(frameRequest); - frameRequest = requestAnimationFrame((frameTime) => func.call(undefined, frameTime)); - }, - cancelFrame: () => cancelAnimationFrame(frameRequest) - }; -} +const small_utils_1 = require("./small-utils"); function virtualOverflowCalcVisibleRect(element) { const elementRect = element.getBoundingClientRect(); const visibleRect = { @@ -204,7 +201,7 @@ function useCalcVirtualOverflow(params, deps) { leftStartIndex: 0, lengthX: 0, }); - const { requestFrame: updateViewRect, cancelFrame } = (0, react_1.useMemo)(() => debounceAnimationFrame((frameTime) => { + const { requestFrame: updateViewRect, cancelFrame } = (0, react_1.useMemo)(() => (0, small_utils_1.rvoDebounceAnimationFrame)((frameTime) => { if (!containerRef.current) return; const visibleRect = calcVisibleRect(containerRef.current, frameTime); @@ -254,6 +251,7 @@ function useVirtualOverflowY(params, deps = []) { return { renderedItems: utilRenderItems1D(itemSlice.topStartIndex, itemSlice.lengthY, params.itemHeight, params.renderItem), updateViewRect, + itemSlice, }; } exports.useVirtualOverflowY = useVirtualOverflowY; @@ -262,6 +260,7 @@ function useVirtualOverflowX(params, deps = []) { return { renderedItems: utilRenderItems1D(itemSlice.leftStartIndex, itemSlice.lengthX, params.itemWidth, params.renderItem), updateViewRect, + itemSlice, }; } exports.useVirtualOverflowX = useVirtualOverflowX; @@ -277,12 +276,29 @@ function useVirtualOverflowGrid(params, deps = []) { } return { renderedItems, - updateViewRect + updateViewRect, + itemSlice }; } exports.useVirtualOverflowGrid = useVirtualOverflowGrid; -},{"react":15}],5:[function(require,module,exports){ +},{"./small-utils":5,"react":16}],5:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.rvoDebounceAnimationFrame = void 0; +function rvoDebounceAnimationFrame(func) { + let frameRequest = 0; + return { + requestFrame: () => { + cancelAnimationFrame(frameRequest); + frameRequest = requestAnimationFrame((frameTime) => func.call(undefined, frameTime)); + }, + cancelFrame: () => cancelAnimationFrame(frameRequest), + }; +} +exports.rvoDebounceAnimationFrame = rvoDebounceAnimationFrame; + +},{}],6:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.virtualOverflowUtils = void 0; @@ -371,7 +387,7 @@ var virtualOverflowUtils; virtualOverflowUtils.calcVisibleRectOverflowed = calcVisibleRectOverflowed; })(virtualOverflowUtils || (exports.virtualOverflowUtils = virtualOverflowUtils = {})); -},{}],6:[function(require,module,exports){ +},{}],7:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; @@ -557,7 +573,7 @@ process.chdir = function (dir) { }; process.umask = function() { return 0; }; -},{}],7:[function(require,module,exports){ +},{}],8:[function(require,module,exports){ (function (process){(function (){ /** * @license React @@ -30429,7 +30445,7 @@ if ( } }).call(this)}).call(this,require('_process')) -},{"_process":6,"react":15,"scheduler":19}],8:[function(require,module,exports){ +},{"_process":7,"react":16,"scheduler":20}],9:[function(require,module,exports){ /** * @license React * react-dom.production.min.js @@ -30754,7 +30770,7 @@ exports.hydrateRoot=function(a,b,c){if(!ol(a))throw Error(p(405));var d=null!=c& e);return new nl(b)};exports.render=function(a,b,c){if(!pl(b))throw Error(p(200));return sl(null,a,b,!1,c)};exports.unmountComponentAtNode=function(a){if(!pl(a))throw Error(p(40));return a._reactRootContainer?(Sk(function(){sl(null,null,a,!1,function(){a._reactRootContainer=null;a[uf]=null})}),!0):!1};exports.unstable_batchedUpdates=Rk; exports.unstable_renderSubtreeIntoContainer=function(a,b,c,d){if(!pl(c))throw Error(p(200));if(null==a||void 0===a._reactInternals)throw Error(p(38));return sl(a,b,c,!1,d)};exports.version="18.2.0-next-9e3b772b8-20220608"; -},{"react":15,"scheduler":19}],9:[function(require,module,exports){ +},{"react":16,"scheduler":20}],10:[function(require,module,exports){ (function (process){(function (){ 'use strict'; @@ -30783,7 +30799,7 @@ if (process.env.NODE_ENV === 'production') { } }).call(this)}).call(this,require('_process')) -},{"_process":6,"react-dom":10}],10:[function(require,module,exports){ +},{"_process":7,"react-dom":11}],11:[function(require,module,exports){ (function (process){(function (){ 'use strict'; @@ -30825,7 +30841,7 @@ if (process.env.NODE_ENV === 'production') { } }).call(this)}).call(this,require('_process')) -},{"./cjs/react-dom.development.js":7,"./cjs/react-dom.production.min.js":8,"_process":6}],11:[function(require,module,exports){ +},{"./cjs/react-dom.development.js":8,"./cjs/react-dom.production.min.js":9,"_process":7}],12:[function(require,module,exports){ (function (process){(function (){ /** * @license React @@ -32143,7 +32159,7 @@ exports.jsxs = jsxs; } }).call(this)}).call(this,require('_process')) -},{"_process":6,"react":15}],12:[function(require,module,exports){ +},{"_process":7,"react":16}],13:[function(require,module,exports){ /** * @license React * react-jsx-runtime.production.min.js @@ -32156,7 +32172,7 @@ exports.jsxs = jsxs; 'use strict';var f=require("react"),k=Symbol.for("react.element"),l=Symbol.for("react.fragment"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0}; function q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=""+g);void 0!==a.key&&(e=""+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}exports.Fragment=l;exports.jsx=q;exports.jsxs=q; -},{"react":15}],13:[function(require,module,exports){ +},{"react":16}],14:[function(require,module,exports){ (function (process){(function (){ /** * @license React @@ -34899,7 +34915,7 @@ if ( } }).call(this)}).call(this,require('_process')) -},{"_process":6}],14:[function(require,module,exports){ +},{"_process":7}],15:[function(require,module,exports){ /** * @license React * react.production.min.js @@ -34927,7 +34943,7 @@ exports.useCallback=function(a,b){return U.current.useCallback(a,b)};exports.use exports.useInsertionEffect=function(a,b){return U.current.useInsertionEffect(a,b)};exports.useLayoutEffect=function(a,b){return U.current.useLayoutEffect(a,b)};exports.useMemo=function(a,b){return U.current.useMemo(a,b)};exports.useReducer=function(a,b,e){return U.current.useReducer(a,b,e)};exports.useRef=function(a){return U.current.useRef(a)};exports.useState=function(a){return U.current.useState(a)};exports.useSyncExternalStore=function(a,b,e){return U.current.useSyncExternalStore(a,b,e)}; exports.useTransition=function(){return U.current.useTransition()};exports.version="18.2.0"; -},{}],15:[function(require,module,exports){ +},{}],16:[function(require,module,exports){ (function (process){(function (){ 'use strict'; @@ -34938,7 +34954,7 @@ if (process.env.NODE_ENV === 'production') { } }).call(this)}).call(this,require('_process')) -},{"./cjs/react.development.js":13,"./cjs/react.production.min.js":14,"_process":6}],16:[function(require,module,exports){ +},{"./cjs/react.development.js":14,"./cjs/react.production.min.js":15,"_process":7}],17:[function(require,module,exports){ (function (process){(function (){ 'use strict'; @@ -34949,7 +34965,7 @@ if (process.env.NODE_ENV === 'production') { } }).call(this)}).call(this,require('_process')) -},{"./cjs/react-jsx-runtime.development.js":11,"./cjs/react-jsx-runtime.production.min.js":12,"_process":6}],17:[function(require,module,exports){ +},{"./cjs/react-jsx-runtime.development.js":12,"./cjs/react-jsx-runtime.production.min.js":13,"_process":7}],18:[function(require,module,exports){ (function (process,setImmediate){(function (){ /** * @license React @@ -35587,7 +35603,7 @@ if ( } }).call(this)}).call(this,require('_process'),require("timers").setImmediate) -},{"_process":6,"timers":20}],18:[function(require,module,exports){ +},{"_process":7,"timers":21}],19:[function(require,module,exports){ (function (setImmediate){(function (){ /** * @license React @@ -35610,7 +35626,7 @@ exports.unstable_scheduleCallback=function(a,b,c){var d=exports.unstable_now();" exports.unstable_shouldYield=M;exports.unstable_wrapCallback=function(a){var b=y;return function(){var c=y;y=b;try{return a.apply(this,arguments)}finally{y=c}}}; }).call(this)}).call(this,require("timers").setImmediate) -},{"timers":20}],19:[function(require,module,exports){ +},{"timers":21}],20:[function(require,module,exports){ (function (process){(function (){ 'use strict'; @@ -35621,7 +35637,7 @@ if (process.env.NODE_ENV === 'production') { } }).call(this)}).call(this,require('_process')) -},{"./cjs/scheduler.development.js":17,"./cjs/scheduler.production.min.js":18,"_process":6}],20:[function(require,module,exports){ +},{"./cjs/scheduler.development.js":18,"./cjs/scheduler.production.min.js":19,"_process":7}],21:[function(require,module,exports){ (function (setImmediate,clearImmediate){(function (){ var nextTick = require('process/browser.js').nextTick; var apply = Function.prototype.apply; @@ -35700,4 +35716,4 @@ exports.clearImmediate = typeof clearImmediate === "function" ? clearImmediate : delete immediateIds[id]; }; }).call(this)}).call(this,require("timers").setImmediate,require("timers").clearImmediate) -},{"process/browser.js":6,"timers":20}]},{},[1]); +},{"process/browser.js":7,"timers":21}]},{},[1]); diff --git a/package.json b/package.json index 51b2c16..8e1241f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-virtual-overflow", - "version": "1.1.0", + "version": "1.1.2", "description": "virtual scroll without headache", "main": "lib/index.cjs.js", "module": "lib/index.esm.js", diff --git a/readme.md b/readme.md index 5282fdf..4a4d221 100644 --- a/readme.md +++ b/readme.md @@ -10,6 +10,7 @@ Similar to [react-virtualized](https://github.com/bvaughn/react-virtualized), bu - No magical divs will wrap your list with position: absolute and height: 0 - No scroll syncing problems - No AutoWindow over AutoSize with VerticalSpecialList +- Dead simple infinity loader - Full rendering controll - It just works - ~0.5kb gzipped @@ -283,6 +284,13 @@ And returns: // method that will force update calculations updateViewRect: () => void, + + itemSlice: { + topStartIndex: number; + lengthY: number; + leftStartIndex: number; + lengthX: number; + } } ``` @@ -331,6 +339,13 @@ And returns: // method that will force update calculations updateViewRect: () => void, + + itemSlice: { + topStartIndex: number; + lengthY: number; + leftStartIndex: number; + lengthX: number; + } } ``` @@ -385,6 +400,13 @@ And returns: // method that will force update calculations updateViewRect: () => void, + + itemSlice: { + topStartIndex: number; + lengthY: number; + leftStartIndex: number; + lengthX: number; + } } ``` @@ -507,6 +529,40 @@ const verticalSlice = virtualOverflowCalcItems( + + +
+ +Infinity loader + + +
+ +All hooks (`useCalcVirtualOverflow`, `useVirtualOverflowY`, `useVirtualOverflowX`, `useVirtualOverflowGrid`) returns `itemSlice` which you can use to trigger infinity loading. + +For example: + +```tsx +const [items, setItems] = useState([] as any[]); + +// here we get current rendered itemSlice +const { renderedItems, itemSlice } = useVirtualOverflowY({ + itemsLengthY: items.length, + // ... +}); + +// here we check if we render bottom range of items +useEffect(() => { + if (itemSlice.topStartIndex + itemSlice.lengthY >= items.length - 4) { + // load more + setItems((prev) => [...prev, ...newItems]); + } +}, [itemSlice.topStartIndex, itemSlice.lengthY]); +``` + +
+ + ### utils All methods here are inside `virtualOverflowUtils` namespace in `react-virtual-overflow/utils`. I will not write namespace here below for readability purposes. diff --git a/src/examples/demo.tsx b/src/examples/demo.tsx index 40e0ce4..673a221 100644 --- a/src/examples/demo.tsx +++ b/src/examples/demo.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import ReactDOM from "react-dom/client"; import { useVirtualOverflowY, virtualOverflowCalcVisibleRect } from ".."; import { virtualOverflowUtils } from "../utils"; @@ -9,13 +9,13 @@ const itemsLine = Array.from({ length: 300 }).map((_, i) => `item ${i}`); const itemsGrid = Array.from({ length: 300 }).map((_, iy) => Array.from({ length: 300 }).map((_, ix) => `item ${ix} ${iy}`)); function ListWithHookExample() { - const items = itemsLine; + const [items, setItems] = useState([] as string[]); const containerRef = useRef(undefined!); const infoRef = useRef(undefined!); const itemHeight = 40; - const { renderedItems, updateViewRect } = useVirtualOverflowY({ + const { renderedItems, updateViewRect, itemSlice } = useVirtualOverflowY({ containerRef, itemHeight, itemsLengthY: items.length, @@ -38,6 +38,13 @@ function ListWithHookExample() { }, 24); }, []); + useEffect(() => { + if (itemSlice.topStartIndex + itemSlice.lengthY >= items.length - 4) { + // load more + setItems(prev => [...prev, ...itemsLine]); + } + }, [itemSlice.topStartIndex, itemSlice.lengthY]); + return ( <>
diff --git a/src/index.tsx b/src/index.tsx index 5f7720c..1dbb2dc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,16 +1,5 @@ import { useLayoutEffect, useMemo, useState } from "react"; - -function debounceAnimationFrame(func: (frameTime: number) => void) { - let frameRequest = 0; - - return { - requestFrame: () => { - cancelAnimationFrame(frameRequest); - frameRequest = requestAnimationFrame((frameTime) => func.call(undefined, frameTime)); - }, - cancelFrame: () => cancelAnimationFrame(frameRequest) - }; -} +import { rvoDebounceAnimationFrame } from "./small-utils"; export type VirtualOverflowVisibleRect = { top: number; @@ -134,7 +123,7 @@ export function useCalcVirtualOverflow(params: UseCalcVirtualOverflowParams, dep lengthX: 0, }); - const { requestFrame: updateViewRect, cancelFrame } = useMemo(() => debounceAnimationFrame((frameTime) => { + const { requestFrame: updateViewRect, cancelFrame } = useMemo(() => rvoDebounceAnimationFrame((frameTime) => { if (!containerRef.current) return; const visibleRect = calcVisibleRect(containerRef.current, frameTime); const newItemSlice = { @@ -191,6 +180,7 @@ export function useVirtualOverflowY(params: UseVirtualOverflowParamsY, deps: any return { renderedItems: utilRenderItems1D(itemSlice.topStartIndex, itemSlice.lengthY, params.itemHeight, params.renderItem), updateViewRect, + itemSlice, }; } @@ -199,6 +189,7 @@ export function useVirtualOverflowX(params: UseVirtualOverflowParamsX, deps: any return { renderedItems: utilRenderItems1D(itemSlice.leftStartIndex, itemSlice.lengthX, params.itemWidth, params.renderItem), updateViewRect, + itemSlice, }; } @@ -217,6 +208,7 @@ export function useVirtualOverflowGrid(params: UseVirtualOverflowParamsGrid, dep return { renderedItems, - updateViewRect + updateViewRect, + itemSlice }; } diff --git a/src/small-utils.ts b/src/small-utils.ts new file mode 100644 index 0000000..bfaf27c --- /dev/null +++ b/src/small-utils.ts @@ -0,0 +1,11 @@ +export function rvoDebounceAnimationFrame(func: (frameTime: number) => void) { + let frameRequest = 0; + + return { + requestFrame: () => { + cancelAnimationFrame(frameRequest); + frameRequest = requestAnimationFrame((frameTime) => func.call(undefined, frameTime)); + }, + cancelFrame: () => cancelAnimationFrame(frameRequest), + }; +}